<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>SandGrid</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://lichuanyang.top/en/</id>
  <link href="https://lichuanyang.top/en/" rel="alternate"/>
  <link href="https://lichuanyang.top/en/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, SandGrid</rights>
  <subtitle>Sand accumulates into pagodas, making every step a choice</subtitle>
  <title>Mobility</title>
  <updated>2026-07-03T07:42:21.746Z</updated>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="ai-agent" scheme="https://lichuanyang.top/en/tags/ai-agent/"/>
    <category term="skill-management" scheme="https://lichuanyang.top/en/tags/skill-management/"/>
    <category term="pks" scheme="https://lichuanyang.top/en/tags/pks/"/>
    <category term="workflow" scheme="https://lichuanyang.top/en/tags/workflow/"/>
    <category term="multi-agent-collaboration" scheme="https://lichuanyang.top/en/tags/multi-agent-collaboration/"/>
    <content>
      <![CDATA[<p>I wrote an article before: <a href="https://lichuanyang.top/en/posts/26060/">https://lichuanyang.top/en/posts/26060/</a> . It taught people how to build Agent-independent workflows, so you can spin up different Agents anytime and smoothly use the free&#x2F;trial plans from various providers. After practicing this way, one thing that obviously becomes tedious is skill management. So I wrote another tool to manage skills across different Agents and projects.</p><span id="more"></span><p>Given the current Agent ecosystem is still quite chaotic, everyone does their own thing, and skill storage directories are defined by each provider.</p><p>For example, Cursor puts them in <code>~/.cursor/skills</code>, Claude Code in <code>~/.claude/skills</code>, Trae in <code>~/.trae/skills</code>, OpenCode in <code>~/.config/opencode/skills</code>, and there are also Windsurf, Qoder, Hermes, etc., each with their own path conventions.</p><p>The community is also trying to define a universal <code>.agents/skill</code> directory, which helps a bit, but not much.</p><p>Of course, the problem of skill management isn’t entirely caused by using multiple Agents. Because skills naturally have very different scopes of application. Some skills are suitable for company projects, some for personal projects, some have an even narrower scope, only applicable to a few related projects, and some skills, like news aggregation, I only want to configure in a specific agent.</p><p>If you ask, is it okay not to do such fine-grained management of skills? Well, there’s no major problem, it’s just that the agent will spend a bit more tokens retrieving skills each time. But as a technical person with OCD, I still want to manage these things more carefully, and just don’t let the agent see these skills in places where they’re not needed.</p><p>Based on this, I designed the pks tool. The core idea is to manage all skills in a centralized place, and then, according to needs, write skills to the agent’s skill directory or project directory.</p><p>Specifically, it supports the following features:</p><p><strong>Global Management</strong>: All skills are centrally stored under <code>~/.local/share/pks/skills/</code>, use <code>pks list</code> to view them, and <code>pks new</code> to create new skills.</p><p><strong>Project-level Installation</strong>: After running <code>pks init</code> in a project directory, you can use <code>pks install</code> to install global skills into the project’s <code>.skills/</code> directory.</p><p><strong>Agent-level Installation</strong>: Use <code>pks install-to &lt;agent&gt; &lt;skill&gt;</code> to directly install skills into a specific agent’s skill directory (like <code>~/.cursor/skills/</code>).</p><p><strong>Bidirectional Sync</strong>: If you modify skill files in a project, use <code>pks push</code> to push the changes back to the global repository.</p><p>In practice, I have about these use cases:</p><p>Some skills have limited scope, like a few related projects. In this case, I don’t put the skill in the agent’s configuration, but instead put it in the project. Using the <code>pks install</code> command, you can put skills from the local skill repository into the project directory. Then you can guide the agent to find skills in the skill directory in the AGENTS.md file. For agents that support project-level skills, you can also use the <code>pks link</code> command to soft-link the agent’s corresponding project-level skill directory, like <code>.opencode/skills</code>, to the skills directory;</p><p>Some skills, like news collection skills, I only want to appear in some agents, so I run the <code>pks install-to</code> command to install it in the agent’s skill directory.</p><p>Sometimes skill files need to be modified. I’ll first modify them in a project, then run <code>pks push</code> to push the changes back to the skill repository.</p><p>This way, there’s a relatively proper and reasonable handling process for skills.</p><p>The project is at: <a href="https://github.com/lcy362/personal-skills-manager">https://github.com/lcy362/personal-skills-manager</a> , welcome to try it out. If you have other experiences with skill management, feel free to share.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/34689/</id>
    <link href="https://lichuanyang.top/en/posts/34689/"/>
    <published>2026-07-01T09:16:24.000Z</published>
    <summary>
      <![CDATA[<p>I wrote an article before: <a href="https://lichuanyang.top/en/posts/26060/">https://lichuanyang.top/en/posts/26060/</a> . It taught people how to build Agent-independent workflows, so you can spin up different Agents anytime and smoothly use the free&#x2F;trial plans from various providers. After practicing this way, one thing that obviously becomes tedious is skill management. So I wrote another tool to manage skills across different Agents and projects.</p>]]>
    </summary>
    <title>From Token Farming to Skill Management: My pks Tool in Practice</title>
    <updated>2026-07-03T07:42:21.746Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="Obsidian" scheme="https://lichuanyang.top/en/tags/Obsidian/"/>
    <category term="llm-wiki" scheme="https://lichuanyang.top/en/tags/llm-wiki/"/>
    <category term="knowledge-management" scheme="https://lichuanyang.top/en/tags/knowledge-management/"/>
    <category term="WeChat-Reading" scheme="https://lichuanyang.top/en/tags/WeChat-Reading/"/>
    <category term="Zhihu" scheme="https://lichuanyang.top/en/tags/Zhihu/"/>
    <category term="AI" scheme="https://lichuanyang.top/en/tags/AI/"/>
    <category term="Notion" scheme="https://lichuanyang.top/en/tags/Notion/"/>
    <content>
      <![CDATA[<p>A while back, I came across Andrej Karpathy’s llm-wiki concept and felt an instant sense of resonance. I’ve always enjoyed writing things down, but the problem was that everything ended up scattered across different places, and I never had the energy to manage it properly. When I discovered llm-wiki, I realized — all that stuff I’d been writing over the years was finally going to pay off.</p><span id="more"></span><h2 id="Why-a-Knowledge-Hub"><a href="#Why-a-Knowledge-Hub" class="headerlink" title="Why a Knowledge Hub"></a>Why a Knowledge Hub</h2><p>First, what is llm-wiki?</p><p>llm-wiki is a concept recently proposed by Andrej Karpathy: take all the written material you’ve accumulated over the years — notes, blog posts, reading highlights, work logs — treat it as a “corpus,” and let an LLM automatically extract concepts, create pages, and weave cross-references into a structured, continuously evolving personal wiki.</p><p>The core premise is simple: everyone produces a substantial amount of structured, insightful writing in their daily work and learning — it’s just scattered everywhere with no connections. llm-wiki uses an LLM-driven process to string these scattered pearls together. You keep producing and collecting content; the LLM handles the organization and management.</p><p>Unlike traditional manual wiki maintenance — creating pages, writing summaries, adding links, tedious and hard to sustain — llm-wiki brings the organizational cost down to nearly zero. You just tell the LLM the structure and rules of your knowledge base (an AGENTS.md file), and it can repeatedly execute ingestion, updating, and auditing operations. My personal experience: watching an AI turn scattered notes into a structured network of cross-references feels like clearing out a long-overdue debt.</p><h2 id="Data-Ingestion"><a href="#Data-Ingestion" class="headerlink" title="Data Ingestion"></a>Data Ingestion</h2><p>The first thing I did was export all my Notion notes — development knowledge, investing insights, and countless miscellaneous records — and move them into Obsidian.</p><p><strong>How to Import Notion Content into Obsidian</strong></p><p>The process is straightforward:</p><ol><li><p><strong>Export your Notion data</strong>: Go to “Settings &amp; members → Settings” in Notion, select “Export all workspace content,” and choose <strong>Markdown &amp; CSV</strong> as the format. You’ll get a ZIP file; after unzipping, each Notion page becomes a <code>.md</code> file, and databases come with additional CSV files. Note: Notion’s free plan exports one workspace at a time — if you have multiple workspaces, export them separately.</p></li><li><p><strong>Install the Obsidian Importer plugin</strong>: Search for “Importer” in the Obsidian community plugin marketplace. It supports one-click import from Notion, Bear, Evernote, OneNote, and more, handling image attachments and internal links automatically. After enabling the plugin, press <code>Cmd+P</code>, search for “Importer: Open Importer,” select the Notion format, and point it to the unzipped folder.</p></li><li><p><strong>Manual import (fallback)</strong>: If you prefer not to use the Importer plugin, just drop the unzipped folder directly into your Obsidian vault. Obsidian natively supports <code>[[wiki-link]]</code> internal links, and Notion’s exported Markdown typically already converts links to this format.</p></li><li><p><strong>Post-import handling</strong>: I recommend placing the original files in a dedicated subdirectory (e.g., <code>raw/notion-export/</code>) and marking them as “read-only.” This preserves the integrity of the original data — a key tenet of the llm-wiki methodology: raw materials are never modified; the LLM builds structured knowledge on top of them. If your Notion had databases, keep the CSV files for reference; embedded Notion-specific blocks (calendars, kanban boards) will lose interactivity after export, but the text content remains.</p></li></ol><p>After the whole process, thousands of scattered notes were consolidated into Obsidian, becoming the first batch of “raw materials” for my knowledge hub.</p><p>Next, I fed Karpathy’s gist to an AI and had it generate the project’s AGENTS.md document. The AI naturally figured out the ingestion and auditing operations required for llm-wiki.</p><p>Then it was time to execute. Watching the AI continuously generate wiki content, categorizing years of accumulated material — it was genuinely satisfying.</p><p>After that, I did a few more things: bringing in my Zhihu writings and WeChat Reading notes. I’ve written over a thousand answers on Zhihu, and over the years I’ve read more than a hundred books on WeChat Reading. Beyond just highlights, these are significant components of my knowledge system. Coincidentally, around that time, WeChat Reading released their official skill, so I put it to use.</p><h2 id="Importing-Zhihu-Content"><a href="#Importing-Zhihu-Content" class="headerlink" title="Importing Zhihu Content"></a>Importing Zhihu Content</h2><p><strong>How to Sync Zhihu Writings to Obsidian</strong></p><p>Zhihu doesn’t provide an official data export API, so I used Playwright for browser automation.</p><p><strong>Steps</strong>:</p><ol><li>Install Playwright: <code>pip install playwright &amp;&amp; playwright install chromium</code></li><li>Run the script for the first time, log in by scanning a QR code or entering your password in the opened Chromium browser</li><li>Once logged in, the script automatically crawls your profile to capture all answers, articles, and status updates</li><li>Login state is persisted locally; subsequent runs use <code>--reuse</code> for silent execution without re-login</li></ol><p><strong>Features</strong>: Incremental sync — only fetches new content, existing files are never reprocessed. Files are organized by content type (answers&#x2F;articles&#x2F;pins).</p><h2 id="Syncing-WeChat-Reading-Notes"><a href="#Syncing-WeChat-Reading-Notes" class="headerlink" title="Syncing WeChat Reading Notes"></a>Syncing WeChat Reading Notes</h2><p><strong>How to Sync WeChat Reading Notes to Obsidian</strong></p><p>WeChat Reading provides an Agent API Gateway — apply for an API key and you’re good to go.</p><p><strong>Steps</strong>:</p><ol><li>Call the <code>/user/notebooks</code> endpoint to get the list of books with notes</li><li>For each new book, fetch highlights and annotations separately</li><li>Group content by chapter and output as well-formatted Markdown files</li></ol><p><strong>Output format</strong>: Book title and author as the heading, each chapter’s highlights in blockquote format (with dates), personal annotations placed below the corresponding highlights.</p><p><strong>Features</strong>: Fully incremental — the script maintains a state file of synced book IDs, only processing new additions on each run. Over 150 books’ worth of notes silently flowed into Obsidian, becoming one of the richest sources of raw material for my knowledge hub.</p><h2 id="Intelligent-Retrieval-with-LLM-Wiki"><a href="#Intelligent-Retrieval-with-LLM-Wiki" class="headerlink" title="Intelligent Retrieval with LLM Wiki"></a>Intelligent Retrieval with LLM Wiki</h2><p>At this point, the content layer was essentially ready. Then I started thinking: since most of my knowledge and creative output is here, could I start distilling… myself?</p><p>I built a simple first version: a “personal” pipeline parallel to the wiki pipeline, with similar ingestion and linting operations. The key difference: wiki focuses on knowledge, while personal focuses on who I am as an individual.</p><p><strong>Knowledge Base vs. Personality Distillation: Two Different AI Processing Approaches</strong></p><p>Here it’s worth explaining the difference — they share the same set of raw materials but have entirely different goals and outputs.</p><p><strong>Knowledge Base (Wiki): Answers “What Do I Know?”</strong></p><p>Extracts objective knowledge from notes, blogs, and reading highlights, generating concept pages (e.g., “distributed consensus”), entity pages (e.g., “Raft algorithm”), and source summary pages (e.g., “Designing Data-Intensive Applications — reading notes”), with dense cross-references between them. The goal: make knowledge queryable and reusable — an externalized second brain.</p><p><strong>Personality Distillation (Personal Model): Answers “Who Am I?”</strong></p><p>Reverse-engineers cognitive patterns, expressive styles, and value orientations from your writing and reading. For example, analyzing technical blog posts might reveal a “thesis-first, case-driven” style; analyzing Zhihu answers might uncover recurring traits like “first-principles reduction” and “quantitative thinking.” The output isn’t knowledge entries — it’s a cognitive map of a person: what you’re good at, how you approach problems, what you value.</p><p><strong>Comparison</strong></p><table><thead><tr><th>Dimension</th><th>Knowledge Base</th><th>Personality Distillation</th></tr></thead><tbody><tr><td>Core question</td><td>What do I know?</td><td>Who am I?</td></tr><tr><td>Input</td><td>Notes, blogs, reading highlights</td><td>All personal writing and reading records</td></tr><tr><td>Output</td><td>Concept&#x2F;entity&#x2F;source pages + cross-references</td><td>Domain depth, cognitive traits, expressive style, values</td></tr><tr><td>Direction</td><td>Outward: structuring external knowledge</td><td>Inward: modeling personal cognition</td></tr><tr><td>Workflow</td><td>Ingest → Query → Lint → Audit</td><td>Ingest → Query → Lint → Audit (isomorphic)</td></tr></tbody></table><p>Both processes are structurally similar, but one looks outward, structuring and organizing the knowledge you possess; the other looks inward, distilling and modeling your cognitive traits as an individual. This “two sides of the same coin” design is, I think, the most fascinating part of the entire system.</p><p>Lately I’ve been looking at projects like Nüwa online to see if there are better approaches to personality distillation.</p><h2 id="Results-and-Reflections"><a href="#Results-and-Reflections" class="headerlink" title="Results and Reflections"></a>Results and Reflections</h2><p>That’s the recent story of my knowledge hub. If you have thoughts or ideas, I’d love to hear them.</p><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-Is-Obsidian-suitable-for-programmers-doing-knowledge-management"><a href="#Q-Is-Obsidian-suitable-for-programmers-doing-knowledge-management" class="headerlink" title="Q: Is Obsidian suitable for programmers doing knowledge management?"></a>Q: Is Obsidian suitable for programmers doing knowledge management?</h3><p>Absolutely. Obsidian’s core philosophy — local Markdown files, bidirectional links, graph visualization — naturally aligns with how programmers work. You already know Markdown syntax. Local file storage means full data control and Git versioning. Bidirectional links let you manage knowledge references just like code dependencies. Layer on the llm-wiki approach, and AI can automatically extract concepts from scattered notes, create pages with cross-references, transforming loose documents into a structured knowledge network.</p><h3 id="Q-Does-LLM-Wiki-require-a-GPU"><a href="#Q-Does-LLM-Wiki-require-a-GPU" class="headerlink" title="Q: Does LLM Wiki require a GPU?"></a>Q: Does LLM Wiki require a GPU?</h3><p>No local GPU deployment is needed. The core idea of LLM Wiki is <strong>letting an LLM process your text</strong>, not running a model yourself. You simply call a cloud LLM API, feed it your Markdown files, and let it extract concepts, generate pages, and build cross-references. The entire “hardware” requirement is just Obsidian plus any tool that can call an LLM API (like WorkBuddy or another Agent).</p><h3 id="Q-What’s-the-difference-between-llm-wiki-and-a-traditional-wiki"><a href="#Q-What’s-the-difference-between-llm-wiki-and-a-traditional-wiki" class="headerlink" title="Q: What’s the difference between llm-wiki and a traditional wiki?"></a>Q: What’s the difference between llm-wiki and a traditional wiki?</h3><p>A traditional wiki requires you to manually create pages, write summaries, and add internal links — high maintenance cost that’s hard to sustain. llm-wiki reduces the organizational cost to nearly zero — you just keep producing and collecting written content, and the LLM reads your AGENTS.md rules, repeatedly running ingestion, updating, and auditing operations to generate a structured cross-reference network. In short: a traditional wiki means “you organize knowledge”; llm-wiki means “AI organizes knowledge for you.”</p><h3 id="Q-What’s-the-difference-between-knowledge-distillation-and-personality-distillation"><a href="#Q-What’s-the-difference-between-knowledge-distillation-and-personality-distillation" class="headerlink" title="Q: What’s the difference between knowledge distillation and personality distillation?"></a>Q: What’s the difference between knowledge distillation and personality distillation?</h3><p>Both share the same set of raw materials, but their goals and outputs are entirely different. <strong>Knowledge Base (Wiki)</strong> answers “What do I know?” — extracting objective knowledge from notes and reading highlights, generating concept pages and cross-references. <strong>Personality Distillation (Personal Model)</strong> answers “Who am I?” — reverse-engineering your cognitive patterns, expressive style, and value orientations from your writing and reading records. One looks outward (structuring knowledge), the other looks inward (modeling personal cognition). The workflows are similar, but the direction is opposite.</p><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><ol><li><strong>Install Obsidian</strong>: Download the client from <a href="https://obsidian.md/">obsidian.md</a> and create a local Vault — just a local folder.</li><li><strong>Configure LLM Wiki</strong>: Create <code>AGENTS.md</code> in the Vault root, following Karpathy’s llm-wiki approach to define maintenance rules, including ingestion, updating, and auditing workflows. Let AI tools read this file to automatically extract concepts and build cross-references from raw notes.</li><li><strong>Import Notion notes</strong>: Export from Notion settings as Markdown + CSV, use the Obsidian Importer plugin for one-click import, or drop the extracted Markdown folder directly into the Vault.</li><li><strong>Connect WeChat Reading</strong>: Apply for a WeChat Reading API key, call the <code>/user/notebooks</code> endpoint to fetch books, pull highlights and notes, group by chapter, and output as Markdown files into the Vault.</li><li><strong>Import Zhihu and blog</strong>: Use a Playwright script to auto-scrape Zhihu answers and articles; copy blog Markdown source files into the Vault. Once done, run a full wiki ingestion via AI to generate the complete cross-reference network.</li></ol><p>Source: <a href="https://lichuanyang.top/en/posts/18804/">https://lichuanyang.top/en/posts/18804/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/18804/</id>
    <link href="https://lichuanyang.top/en/posts/18804/"/>
    <published>2026-06-26T08:18:13.000Z</published>
    <summary>
      <![CDATA[<p>A while back, I came across Andrej Karpathy’s llm-wiki concept and felt an instant sense of resonance. I’ve always enjoyed writing things down, but the problem was that everything ended up scattered across different places, and I never had the energy to manage it properly. When I discovered llm-wiki, I realized — all that stuff I’d been writing over the years was finally going to pay off.</p>]]>
    </summary>
    <title>Bringing Notes, WeChat Reading, and Zhihu into Obsidian: My LLM-Wiki Knowledge Hub</title>
    <updated>2026-06-27T03:23:24.826Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="ai-video" scheme="https://lichuanyang.top/en/tags/ai-video/"/>
    <category term="open-source" scheme="https://lichuanyang.top/en/tags/open-source/"/>
    <category term="free-tools" scheme="https://lichuanyang.top/en/tags/free-tools/"/>
    <category term="text-to-video" scheme="https://lichuanyang.top/en/tags/text-to-video/"/>
    <category term="agnes-ai" scheme="https://lichuanyang.top/en/tags/agnes-ai/"/>
    <content>
      <![CDATA[<blockquote><p>“The solution is not to suppress AI, but to make it a more equitable capability, so that everyone knows how to create more with AI. This is a very important vision for our company — to make world-class AI belong to everyone.”</p></blockquote><p>This is something Bruce Yang, the founder of Agnes AI, said in an interview.</p><p>Many Chinese AI companies today — DeepSeek, Zhipu, and others — are driving down the price of AI. To be fair, the cost of text and code processing has already been pushed remarkably low. But video is different. Making AI videos today has an absurdly high barrier — overseas services like Runway and Pika charge tens of dollars monthly, domestic platforms like Jimeng and Keling charge by the second once free quotas run out, and running open-source models locally requires a GPU costing over ten thousand RMB.</p><p>Objectively speaking, video generation is genuinely expensive right now. Making industrial-grade video generation available to everyone isn’t realistic. But ordinary people should still have ways to experiment and create. Thanks to Agnes for opening up their video model and giving us this opportunity. This project is just a small contribution toward that goal. <a href="https://github.com/lcy362/agnes-video-generator">Agnes Video Generator</a> (<a href="https://video.lichuanyang.top/">official website</a>) — it’s a free AI video generator. Not “free trial” or “free for 3 generations,” but the whole thing: script writing, image synthesis, video rendering, voiceover, subtitles, all at zero cost. You just need a free API key from <a href="https://platform.agnes-ai.com/">Agnes AI</a>.</p><p>Agnes’s video model isn’t perfect yet, to be honest. But I want to use this project to grow alongside Agnes, and contribute in my own small way toward AI equity.</p><span id="more"></span><h2 id="Multiple-Ways-to-Use-It"><a href="#Multiple-Ways-to-Use-It" class="headerlink" title="Multiple Ways to Use It"></a>Multiple Ways to Use It</h2><p>Give it a text prompt, get a video back. A few different modes:</p><p><strong>Simple Video.</strong> A straightforward API wrapper — good for testing. Most API parameters are exposed as config options.</p><p><strong>Creative Video.</strong> You write a story idea, like “dark version of The Frog Prince,” and the AI handles everything: expand story → generate character references → split into scenes → write shot prompts → generate per-scene video → narration → subtitles → final output. Ten steps, all automatic. By pre-generating end frames, it ensures the best possible visual continuity between scenes.</p><p><strong>Manuscript Video &amp; Digital Anchor.</strong> Paste a long article or script — it auto-splits by speech duration and generates video per segment, or puts a digital anchor there to read it. Everything stitched with a unified TTS narration + subtitle track. Great for explainers and course content.</p><p>For detailed parameters and usage guides for each mode, check the <a href="https://video.lichuanyang.top/">official website</a>.</p><h2 id="Getting-It-Running"><a href="#Getting-It-Running" class="headerlink" title="Getting It Running"></a>Getting It Running</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/lcy362/agnes-video-generator.git</span><br><span class="line"><span class="built_in">cd</span> agnes-video-generator</span><br><span class="line">./start.sh</span><br></pre></td></tr></table></figure><p>That’s it. <code>start.sh</code> creates a virtual environment, installs dependencies, and starts the server.</p><p>Once it’s running, open <code>http://localhost:8765</code>, paste in your Agnes AI API key at the top, pick a mode, write your idea, and wait patiently for the results.</p><p>If you’re using an AI coding assistant like Cursor or Claude, I’ve included a dedicated guide for AI Agents. Just tell your agent to read the <code>Agents.md</code> file in the project — it’ll handle the whole setup on its own.</p><h2 id="Demos"><a href="#Demos" class="headerlink" title="Demos"></a>Demos</h2><p>I made a few demos — check them out:</p><ul><li><a href="https://v.douyin.com/L4F6KdGnD6U/">The Frog Prince — no narration</a> — 5 scenes, keyframes chaining, fully auto-generated</li><li><a href="https://v.douyin.com/l2FlbF1Jdz0/">Same story, with voiceover and subtitles</a> — AI narration + auto subtitles, see the subtitle effect</li><li><a href="https://v.douyin.com/eSGE9KENWVU/">Manuscript video</a> — pasted a long article, auto-split with different visuals per segment</li></ul><h2 id="That’s-About-It"><a href="#That’s-About-It" class="headerlink" title="That’s About It"></a>That’s About It</h2><p>Going back to Bruce Yang’s words — “making world-class AI belong to everyone.”</p><p>This project isn’t some grand mission. It’s just about keeping the door to AI video creation open. No subscription, no fancy GPU, no cost at all — just a free API key and a machine that can run Python.</p><p>Code on <a href="https://github.com/lcy362/agnes-video-generator">GitHub</a>, official website at <a href="https://video.lichuanyang.top/">video.lichuanyang.top</a>. Bug reports welcome.</p><p>Source: <a href="https://lichuanyang.top/en/posts/22470/">https://lichuanyang.top/en/posts/22470/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/22470/</id>
    <link href="https://lichuanyang.top/en/posts/22470/"/>
    <published>2026-06-16T05:21:16.000Z</published>
    <summary>Tired of paying per second for AI video? I open-sourced a 100% free AI video generator — text, images, video, and voiceover all at zero cost. One click to produce narrated, subtitled multi-scene AI videos.</summary>
    <title>Free AI Video Generator: How I Built a Zero-Cost Tool for Narrated Multi-Scene Videos</title>
    <updated>2026-06-27T03:23:24.827Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="agnes" scheme="https://lichuanyang.top/en/tags/agnes/"/>
    <category term="agnes free model" scheme="https://lichuanyang.top/en/tags/agnes-free-model/"/>
    <category term="agnes video" scheme="https://lichuanyang.top/en/tags/agnes-video/"/>
    <category term="Agnes-Video-V2.0" scheme="https://lichuanyang.top/en/tags/Agnes-Video-V2-0/"/>
    <category term="AI video generation" scheme="https://lichuanyang.top/en/tags/AI-video-generation/"/>
    <category term="free AI models" scheme="https://lichuanyang.top/en/tags/free-AI-models/"/>
    <category term="open source" scheme="https://lichuanyang.top/en/tags/open-source/"/>
    <category term="ViMax" scheme="https://lichuanyang.top/en/tags/ViMax/"/>
    <content>
      <![CDATA[<p>A while back, I wrote about “token hunting” — the practice of bouncing between free AI models instead of paying for subscriptions. At the time, free models mostly meant text and maybe some image generation. Then Agnes AI quietly dropped something unexpected: <strong>free video generation models.</strong></p><p>Not text. Not images. Video. Three models — <code>agnes-video-v2.0</code>, <code>agnes-image-2.1-flash</code>, and <code>agnes-2.0-flash</code> — all free, no credit card, no GPU, just an API key. Among free agnes video generation solutions, Agnes-Video-V2.0 stands out as the most capable, supporting multiple generation modes with impressive output quality.</p><p>Naturally, I couldn’t resist. I took the open-source ViMax framework and rebuilt it as a full Agnes-powered pipeline, with a bunch of usability improvements along the way.</p><span id="more"></span><h2 id="What-Agnes-Free-Models-Actually-Give-You"><a href="#What-Agnes-Free-Models-Actually-Give-You" class="headerlink" title="What Agnes Free Models Actually Give You"></a>What Agnes Free Models Actually Give You</h2><p>Let’s start with what’s on the table. After <a href="https://platform.agnes-ai.com/">signing up at Agnes AI</a>, you get an API key that unlocks three models:</p><ul><li><strong>agnes-2.0-flash</strong> (Chat): Writes stories, scripts, and visual prompts from a single idea</li><li><strong>agnes-image-2.1-flash</strong> (Image): Text-to-image for character references and keyframes</li><li><strong>agnes-video-v2.0</strong> (Video): Supports text-to-video, image-to-video, and keyframes modes</li></ul><p>The API follows the OpenAI-compatible format at <code>https://apihub.agnes-ai.com/v1</code>, making integration straightforward. The video model is async — submit a task, get a task_id, poll for results. Standard stuff for cloud video generation.</p><p>The real kicker: <strong>these three models cover the entire pipeline from idea to finished video.</strong> No mixing providers, no juggling multiple API keys. One key, one base URL, done.</p><h2 id="The-Rewrite-From-Multi-Provider-to-Single-Agnes-Key"><a href="#The-Rewrite-From-Multi-Provider-to-Single-Agnes-Key" class="headerlink" title="The Rewrite: From Multi-Provider to Single Agnes Key"></a>The Rewrite: From Multi-Provider to Single Agnes Key</h2><p>The original ViMax, open-sourced by HKU, is a well-designed agentic video generation framework. It’s intentionally provider-agnostic — one service for LLM, another for images, yet another for video. Flexible, yes, but you end up managing three sets of credentials, three error handlers, three rate limit strategies.</p><p>My approach was simple: <strong>since all three Agnes models are free, just use Agnes for everything.</strong> One API key, one base URL, minimal headache.</p><p>Here’s what changed:</p><p><strong>Screenwriter module</strong>: All LLM calls now hit <code>agnes-2.0-flash</code>. It takes a one-line idea and produces a full story, breaks it into scenes, writes visual prompts for each scene, and generates end-frame descriptions. Standard chat&#x2F;completions endpoint, temperature 0.7.</p><p><strong>Image generator</strong>: Switched to <code>agnes-image-2.1-flash</code> for text-to-image and <code>agnes-image-2.0-flash</code> for image-to-image. The newer 2.1 model generates character reference images; the older 2.0 handles scene transition frames.</p><p><strong>Video generator</strong>: The core — <code>agnes-video-v2.0</code>. Supports three modes: pure text-to-video (t2v), image-guided video (ti2vid), and keyframe interpolation (keyframes). Each scene supports 5 to 20 seconds at 24fps.</p><p>After the rewrite, the entire project depends on a single API provider. Write one key in <code>.api_key</code>, and you’re done.</p><h2 id="Usability-Improvements-Don’t-Make-Users-Think"><a href="#Usability-Improvements-Don’t-Make-Users-Think" class="headerlink" title="Usability Improvements: Don’t Make Users Think"></a>Usability Improvements: Don’t Make Users Think</h2><p>The original ViMax had a very “research project” feel — parameters hardcoded in Python, changing a creative idea meant editing source code. Not great. So I did a bunch of usability work.</p><h3 id="YAML-Creative-Configs"><a href="#YAML-Creative-Configs" class="headerlink" title="YAML Creative Configs"></a>YAML Creative Configs</h3><p>The biggest change: YAML-based creative files. Each video idea lives in its own <code>.yaml</code> file under <code>creatives/</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">&quot;frog&quot;</span></span><br><span class="line"><span class="attr">idea:</span> <span class="string">|</span></span><br><span class="line"><span class="string">  A dark twist on The Frog Prince — the princess kisses</span></span><br><span class="line"><span class="string">  the frog, but instead of a handsome prince, the frog</span></span><br><span class="line"><span class="string">  transforms into an even more terrifying creature</span></span><br><span class="line"><span class="string"></span><span class="attr">user_requirement:</span> <span class="string">|</span></span><br><span class="line"><span class="string">  5 scenes, 10 seconds each, gothic dark fairytale</span></span><br><span class="line"><span class="string"></span><span class="attr">style:</span> <span class="string">&quot;Gothic dark fairytale, cinematic quality&quot;</span></span><br><span class="line"><span class="attr">chaining_mode:</span> <span class="string">keyframes</span></span><br><span class="line"><span class="attr">video_width:</span> <span class="number">768</span></span><br><span class="line"><span class="attr">video_height:</span> <span class="number">1152</span></span><br></pre></td></tr></table></figure><p>New video idea? Write a YAML, run one command, done. No touching Python source code.</p><h3 id="One-Click-Launcher"><a href="#One-Click-Launcher" class="headerlink" title="One-Click Launcher"></a>One-Click Launcher</h3><p><code>start.sh</code> wraps everything: auto-loads the API key from <code>.api_key</code>, activates the virtual environment, lists available creatives, and runs the pipeline. No arguments lists all creatives; pass a name and it just runs:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">./start.sh          <span class="comment"># list all creatives</span></span><br><span class="line">./start.sh frog     <span class="comment"># generate &quot;The Frog Prince&quot;</span></span><br></pre></td></tr></table></figure><h3 id="Smart-Caching-and-Resume"><a href="#Smart-Caching-and-Resume" class="headerlink" title="Smart Caching and Resume"></a>Smart Caching and Resume</h3><p>This one matters because video generation is genuinely slow.</p><p>Every intermediate result — story text, scene scripts, character reference images, per-scene videos — is persisted to disk. If the pipeline crashes halfway, re-running the same creative skips completed steps and only generates what’s missing.</p><p>The caching granularity goes down to individual scene videos: if you had 5 scenes and crashed after 3, re-running only generates the remaining 2. No restart from scratch.</p><h3 id="Multimodal-Image-Analysis"><a href="#Multimodal-Image-Analysis" class="headerlink" title="Multimodal Image Analysis"></a>Multimodal Image Analysis</h3><p>You can provide your own character reference images or custom end-frame images per scene. The system analyzes them via the multimodal LLM and weaves the visual descriptions into the story and prompt generation.</p><p>For example, provide a hand-drawn cartoon character, and the system will identify its visual features and maintain consistency across all generated scenes.</p><h2 id="Character-Consistency-Two-Stage-Lockdown"><a href="#Character-Consistency-Two-Stage-Lockdown" class="headerlink" title="Character Consistency: Two-Stage Lockdown"></a>Character Consistency: Two-Stage Lockdown</h2><p>The biggest pitfall in AI video generation is character consistency — your protagonist has black hair in scene one and suddenly goes blonde in scene two.</p><p>ViMax-Agnes uses a two-stage approach:</p><p><strong>Stage one</strong>: Generate (or accept from the user) a character reference image. The screenwriter module extracts a detailed appearance description from the story — body type, hairstyle, clothing, color palette, distinguishing features — and feeds it to the image model to produce a full-body reference.</p><p><strong>Stage two</strong>: Every scene video starts from this reference image as its first frame, generated via <code>ti2vid</code> mode. The video model animates from the same visual anchor, naturally preserving character consistency.</p><p>In practice, cartoon and stylized art styles get the best consistency. For photorealistic output, providing your own reference image works better than relying on AI generation.</p><h2 id="Three-Scene-Chaining-Modes"><a href="#Three-Scene-Chaining-Modes" class="headerlink" title="Three Scene Chaining Modes"></a>Three Scene Chaining Modes</h2><p>This was the most interesting part of the rewrite. I adapted three chaining modes on top of the Agnes API:</p><p><strong><code>none</code> (Independent)</strong>: Each scene is generated independently, sharing only the character reference image. Fastest, but hard cuts between scenes — no transitions.</p><p><strong><code>ti2vid</code> (Transition Frames)</strong>: Sequential generation. After each scene, the last frame is extracted, then img2img generates a “transition frame” blending the end of one scene into the start of the next. Smoother transitions, but error accumulates — artifacts in earlier scenes propagate forward.</p><p><strong><code>keyframes</code> (Keyframe Interpolation)</strong>: The recommended mode. Each scene specifies both a first frame and a last frame, and the video model interpolates motion between them. End frames are AI-generated from scene descriptions (or manually provided). With both endpoints determined, transitions are the smoothest of the three modes.</p><p>Switching between modes is a single YAML field — no code changes needed.</p><h2 id="Results"><a href="#Results" class="headerlink" title="Results"></a>Results</h2><p>I tested several creative ideas:</p><ul><li><strong>The Frog Prince</strong>: 5-scene dark fairytale, keyframes mode, fully auto-generated</li><li><strong>Girl Dunk</strong>: 3-scene sports theme, character consistency held up well</li><li><strong>Beach Dance</strong>: 4-scene MV style, smooth scene transitions</li><li><strong>Hot Spring Robot</strong>: 3-scene cozy vibe, cartoon style had the best consistency</li></ul><p>The main bottleneck is video generation itself — each scene takes a few minutes. But with the caching system, the debugging cost stays manageable.</p><h2 id="Agnes-Video-V2-0-Technical-Details"><a href="#Agnes-Video-V2-0-Technical-Details" class="headerlink" title="Agnes-Video-V2.0 Technical Details"></a>Agnes-Video-V2.0 Technical Details</h2><p>For those who want to dig deeper, a few implementation notes:</p><p><strong>Video parameters</strong>: Frame counts follow the <code>8n+1</code> rule with a 441-frame cap. 5 seconds &#x3D; 121 frames, 10s &#x3D; 241, 15s &#x3D; 361, 18s and 20s &#x3D; 441 (at 24 and 22 fps respectively).</p><p><strong>Image upload workaround</strong>: The video API needs image URLs, not base64. Local images get “uploaded” through the image API — a no-op i2i call with <code>agnes-image-2.1-flash</code> (prompt: “keep the image exactly as it is”) that returns a hosted URL. Falls back to inline base64 if the upload fails.</p><p><strong>Retry mechanism</strong>: Video submissions auto-retry with exponential backoff on 429 (rate limit) and 5xx (server errors), up to 5 attempts. Polling has no timeout — video generation is just slow, and you have to wait.</p><p><strong>Minimal dependencies</strong>: The entire project uses only 5 Python packages: requests, pydantic, PyYAML, moviepy, and tenacity. No PyTorch, no CUDA — it’s purely an API orchestration layer.</p><h2 id="Wrapping-Up"><a href="#Wrapping-Up" class="headerlink" title="Wrapping Up"></a>Wrapping Up</h2><p>Agnes’s free model lineup is genuinely generous. The agnes free models cover text, image, and video, with an OpenAI-compatible API format and zero signup friction. For anyone curious about AI video generation without burning money, it’s a solid starting point.</p><p>The ViMax-Agnes rewrite confirmed something I’d been thinking: <strong>when free models are good enough, the “hunt for free tokens” strategy scales seamlessly from text to video.</strong> One API key, one command, one YAML file — from a sentence to a complete multi-scene video.</p><p>The project is open source: <a href="https://github.com/lcy362/vimax-agnes">github.com&#x2F;lcy362&#x2F;vimax-agnes</a>. Stars and issues welcome.</p><blockquote><p><strong>Updated June 2026</strong>: This tool has evolved into <a href="https://github.com/lcy362/agnes-video-generator">Agnes Video Generator</a> with Web UI and multilingual support. The new version is more feature-complete — check it out.</p></blockquote><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><ol><li><strong>Setup</strong>: Register an <a href="https://platform.agnes-ai.com/">Agnes AI</a> account and get your API Key. Ensure Python 3.8+ and Git are installed locally.</li><li><strong>Clone the project</strong>: <code>git clone https://github.com/lcy362/vimax-agnes &amp;&amp; cd vimax-agnes</code>, write your API Key to <code>.api_key</code>.</li><li><strong>Create creative config</strong>: Create a YAML file under <code>creatives/</code> defining <code>name</code>, <code>idea</code>, <code>style</code>, <code>chaining_mode</code>, etc.</li><li><strong>Generate video</strong>: Run <code>./start.sh &lt;creative_name&gt;</code>. The system auto-executes script generation, image generation, and video generation in one pipeline, with intermediate results cached for resume support.</li><li><strong>Review output</strong>: Videos are saved under <code>output/</code>. Check character consistency, scene transitions, and overall quality. Tweak the YAML config and rerun if needed.</li></ol><p>Source: <a href="https://lichuanyang.top/en/posts/65500/">https://lichuanyang.top/en/posts/65500/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/65500/</id>
    <link href="https://lichuanyang.top/en/posts/65500/"/>
    <published>2026-06-11T14:00:00.000Z</published>
    <summary>Agnes AI released free video models including Agnes-Video-V2.0 — no GPU, no credit card needed. I rewrote the open-source ViMax framework into a full Agnes pipeline with major usability improvements.</summary>
    <title>Agnes Free Models: I Rewrote ViMax to Generate AI Videos for $0</title>
    <updated>2026-06-27T03:53:05.199Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="ai-agent" scheme="https://lichuanyang.top/en/tags/ai-agent/"/>
    <category term="workflow" scheme="https://lichuanyang.top/en/tags/workflow/"/>
    <category term="llm" scheme="https://lichuanyang.top/en/tags/llm/"/>
    <category term="productivity" scheme="https://lichuanyang.top/en/tags/productivity/"/>
    <category term="cli" scheme="https://lichuanyang.top/en/tags/cli/"/>
    <content>
      <![CDATA[<p>Last post ended with this: <strong>Agents are just tools. Documentation is the core.</strong></p><p>After that went up, someone asked: sure, but how do you actually manage all those docs? You can’t just copy-paste them into every project, can you?</p><p>Nope, you can’t.</p><p>So I built a tool to solve exactly that — <a href="https://github.com/lcy362/personal-skills-manager">pks</a> (Personal Skills Manager). 408 lines of pure bash, zero dependencies. It does one thing: packages your AI workflow docs as skills, installs them into projects on demand, and locks you into no Agent platform.</p><span id="more"></span><h2 id="What-Is-a-Skill"><a href="#What-Is-a-Skill" class="headerlink" title="What Is a Skill"></a>What Is a Skill</h2><p>If you’ve used OpenCode, Cursor, or any other Agent platform, you’ve probably encountered the concept of a “skill.” Platforms call them different things — skills, rules, custom instructions — but they all mean the same thing: <strong>structured instructions you give an Agent so it knows how to handle specific types of tasks.</strong></p><p>“When calling our API gateway, parameters must be flat in the request body — no nesting under <code>params</code>.” That’s a skill. “Commit messages must follow <code>type(scope): description</code> format.” Also a skill. “Table names use snake_case, every column must have a comment.” Still a skill.</p><p>The underlying model provides general-purpose reasoning — it knows how to write code, how to call APIs. But the quirks of your API gateway, your team’s naming conventions, why your project chose this architecture — the model doesn’t know any of that. You have to tell it. Skills are how.</p><p>Each platform manages skills differently. OpenCode uses <code>.opencode/skills/</code> directories, Cursor uses <code>.cursorrules</code>, Claude Code reads <code>CLAUDE.md</code>. Different formats, different mechanisms, same idea: a document the Agent reads and follows.</p><p>Skills themselves don’t depend on any platform. A skill folder written in Markdown, dropped into any project — any Agent that finds it will read it and use it. <strong>Plain Markdown. Anyone can read it.</strong></p><h2 id="Where’s-the-Problem"><a href="#Where’s-the-Problem" class="headerlink" title="Where’s the Problem"></a>Where’s the Problem</h2><p>Skills are great. But here’s the catch: you have multiple projects.</p><p>A team coding standard — project A needs it, project B needs it, project C needs it too. How do you manage that?</p><p>The common approach is to configure it at the Agent level — a standard “install skill” operation. For example, you install a WeRead skill into OpenCode, and boom, every project you open with OpenCode can use it.</p><p>That works fine… unless you read my last post.</p><p>If you’re trying to build agent-independent workflows, this approach falls apart fast. The day you spot a new Agent giving away free tokens, download it, and realize you have to manually migrate all your skills? Kinda defeats the purpose.</p><p>That’s exactly why I built pks — to decouple skill management from any specific Agent or project, and make it a standalone system of its own.</p><h2 id="How-pks-Solves-It"><a href="#How-pks-Solves-It" class="headerlink" title="How pks Solves It"></a>How pks Solves It</h2><p>pks is best suited for <strong>non-general, project- or team-specific</strong> instructions: your coding style preferences and commit conventions, your team’s code standards and CI&#x2F;CD workflows, your project’s architecture decision records and API design constraints.</p><p>pks does one thing: <strong>manage all your skills in one place, install them into any project on demand.</strong></p><p>All skills live in a single global repo. Each project installs only what it needs.</p><p>Three core global commands:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">pks new my-skill       <span class="comment"># Create a new skill skeleton from template</span></span><br><span class="line">pks list               <span class="comment"># List all your skills</span></span><br><span class="line">pks delete my-skill    <span class="comment"># Delete one you don&#x27;t need</span></span><br></pre></td></tr></table></figure><p>Project-level operations are just as simple:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> your-project</span><br><span class="line">pks init                        <span class="comment"># Initialize (one-time only)</span></span><br><span class="line">pks install my-skill            <span class="comment"># Install a skill</span></span><br><span class="line">pks status                      <span class="comment"># See what&#x27;s installed</span></span><br><span class="line">pks uninstall my-skill          <span class="comment"># Remove it</span></span><br></pre></td></tr></table></figure><p>Once installed, the skill gets copied into the project’s <code>.skills/</code> directory:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">your-project/</span><br><span class="line">├── .skills/</span><br><span class="line">│   ├── INDEX.md              # Auto-generated index</span><br><span class="line">│   └── my-skill/</span><br><span class="line">│       └── SKILL.md           # Your skill content</span><br><span class="line">└── ...</span><br></pre></td></tr></table></figure><h2 id="How-Agents-Discover-Skills"><a href="#How-Agents-Discover-Skills" class="headerlink" title="How Agents Discover Skills"></a>How Agents Discover Skills</h2><p>It’s actually a very natural flow.</p><p>Every time you install or uninstall, pks automatically rebuilds <code>.skills/INDEX.md</code>. This index lists all installed skills with descriptions and versions, and includes one line:</p><blockquote><p>Agents: read the <code>SKILL.md</code> file in each skill directory below for relevant instructions.</p></blockquote><p>Any Agent entering the project will see this directory, read INDEX.md, and load the specific SKILL.md files as needed.</p><p>At most, you might add a line in your project’s AGENTS.md pointing to the skills directory. In my experience, most Agents don’t even need that — they find the skills naturally on their own.</p><h2 id="When-Would-You-Use-This"><a href="#When-Would-You-Use-This" class="headerlink" title="When Would You Use This"></a>When Would You Use This</h2><p>Here’s the most common scenario: you’re leading a project and a new developer is joining the team.</p><p>The old way? Hand over a README, verbally explain “we write commits like this,” “that internal library works like that,” “don’t mess with database fields.” The rest? Figure it out yourself.</p><p>With an Agent, it’s about the same — you stuff the rules into CLAUDE.md or AGENTS.md, and the Agent consumes them every single conversation, regardless of whether the current task has anything to do with those rules.</p><p>Try a different approach. Break this knowledge into a few skills:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">skills/</span><br><span class="line">├── team-coding-style/     # Team coding standards, naming conventions, commit format</span><br><span class="line">│   └── SKILL.md</span><br><span class="line">├── internal-api-gateway/  # Internal API gateway: flat params, auth, pitfall notes</span><br><span class="line">│   ├── SKILL.md</span><br><span class="line">│   ├── pitfalls.md        # Common gotchas (e.g., don&#x27;t nest params)</span><br><span class="line">│   └── fields.md          # Response field reference</span><br><span class="line">├── project-architecture/  # Architecture decisions: module layout, directory structure, rationale</span><br><span class="line">│   └── SKILL.md</span><br><span class="line">└── db-conventions/        # Database conventions: naming, indexing strategy, migration process</span><br><span class="line">    └── SKILL.md</span><br></pre></td></tr></table></figure><p>New hire? <code>pks init</code>, <code>pks install team-coding-style</code>, <code>pks install project-architecture</code> — done in minutes. The Agent reads these skills and immediately knows how this project works. Different person, different machine, different Agent platform — same skills, same results.</p><p>And <strong>only installed skills consume tokens.</strong> A pure frontend project doesn’t need the database conventions skill. A legacy project doesn’t need the onboarding skill. Compared to stuffing all rules into one massive config file that the Agent reads every single conversation, installing on demand saves a lot of wasted tokens — which is basically farming your own tokens.</p><p>These <strong>non-general</strong> instructions are pks’s sweet spot. General capabilities? The Agent already has those. But your team standards, your project conventions, the pitfalls you’ve stepped in — those are only useful to you, and should only show up when needed.</p><h2 id="Why-Pure-Bash"><a href="#Why-Pure-Bash" class="headerlink" title="Why Pure Bash"></a>Why Pure Bash</h2><p>The entire pks CLI is 408 lines of bash. No Python, no Node.js, no runtime. <code>git clone</code> and it runs.</p><p>Why so extreme? Because a skill management tool shouldn’t introduce any overhead. Your workflow is already complex enough — the tool managing it should be as simple as possible. Simple enough that it won’t break because some runtime had a version update.</p><p>Bash works out of the box on macOS and Linux. A script from ten years ago still runs today. That’s the beauty of zero dependencies: <strong>your workflow docs outlive any framework.</strong></p><h2 id="A-Few-Design-Details-Worth-Mentioning"><a href="#A-Few-Design-Details-Worth-Mentioning" class="headerlink" title="A Few Design Details Worth Mentioning"></a>A Few Design Details Worth Mentioning</h2><p>pks has a few design choices worth calling out.</p><p><strong>Semantic versioning</strong>: Each skill’s YAML front matter includes a <code>version</code> field. When a skill evolves, the version bumps. You always know which version a project has installed.</p><p><strong>Global management, per-project installation</strong>: All skills maintained in one repo. Projects install only what they need.</p><p><strong>Template protection</strong>: Skills starting with <code>_</code> (like <code>_template</code>) are hidden from listings and can’t be deleted. <code>pks new</code> auto-generates a skeleton from the template — no starting from scratch.</p><p><strong>Path-independent</strong>: pks resolves symlinks and uses relative paths. No matter where you invoke it, it always finds the skills directory. It always knows where home is.</p><h2 id="Back-to-That-Line"><a href="#Back-to-That-Line" class="headerlink" title="Back to That Line"></a>Back to That Line</h2><p>Last post said: “Agents are just tools. Documentation is the core.” This post takes it a step further: <strong>documented workflows can be managed.</strong></p><p>pks isn’t anything fancy — just 408 lines of bash. But it represents an attitude: your workflow is your asset, not any platform’s appendage.</p><p>Farm those tokens. Manage those skills. Saving money and doing good work — those two things aren’t mutually exclusive.</p><p>Source: <a href="https://lichuanyang.top/en/posts/26061/">https://lichuanyang.top/en/posts/26061/</a></p><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-What’s-the-difference-between-Skills-management-and-Agent-management"><a href="#Q-What’s-the-difference-between-Skills-management-and-Agent-management" class="headerlink" title="Q: What’s the difference between Skills management and Agent management?"></a>Q: What’s the difference between Skills management and Agent management?</h3><p>Skills management is <strong>independent of any Agent platform</strong> — you store all skills in a single global repository and install only what each project needs. Agent management means configuring skills inside a specific Agent’s internal settings (e.g., OpenCode’s <code>.opencode/skills/</code> or Cursor’s <code>.cursorrules</code>), tying them to that platform. The benefit of the former is zero migration cost when switching platforms; the latter offers tight integration but requires reconfiguration every time you switch Agents.</p><h3 id="Q-How-is-cross-platform-compatibility-maintained"><a href="#Q-How-is-cross-platform-compatibility-maintained" class="headerlink" title="Q: How is cross-platform compatibility maintained?"></a>Q: How is cross-platform compatibility maintained?</h3><p>The key is <strong>plain Markdown</strong>. When pks installs a skill, it generates <code>.md</code> files placed in the project’s <code>.skills/</code> directory, along with an auto-maintained <code>INDEX.md</code> index file. Any Agent scanning the project directory will discover this structure, read INDEX.md to learn what skills are available, then load specific SKILL.md files as needed. No special formats, no platform SDKs required — plain Markdown is the greatest common divisor.</p><h3 id="Q-How-does-pks-differ-from-configuring-skills-directly-in-an-Agent"><a href="#Q-How-does-pks-differ-from-configuring-skills-directly-in-an-Agent" class="headerlink" title="Q: How does pks differ from configuring skills directly in an Agent?"></a>Q: How does pks differ from configuring skills directly in an Agent?</h3><p>Configuring directly in an Agent is convenient, but skills become “bound” to that Agent. pks’s approach is to <strong>decouple skill management from the Agent</strong> — skills live in a global repo and are installed into any project on demand. The biggest practical benefit: on-demand installation means <strong>only installed skills consume tokens</strong>. A pure frontend project doesn’t need the database conventions skill. A legacy project doesn’t need the onboarding skill. Compared to stuffing all rules into one giant config file the Agent reads every conversation, pks saves a significant amount of wasted tokens.</p><h3 id="Q-Why-use-bash-instead-of-Python-or-Node-js"><a href="#Q-Why-use-bash-instead-of-Python-or-Node-js" class="headerlink" title="Q: Why use bash instead of Python or Node.js?"></a>Q: Why use bash instead of Python or Node.js?</h3><p>Zero dependencies. A skill management tool itself shouldn’t introduce any runtime overhead — your workflow is already complex enough. A 408-line bash script works out of the box on macOS and Linux, requires no interpreter installation, no virtual environment management, and won’t break because of a runtime version upgrade. A bash script from ten years ago still runs today, ensuring your workflow documentation outlives any framework.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/26061/</id>
    <link href="https://lichuanyang.top/en/posts/26061/"/>
    <published>2026-06-04T14:00:00.000Z</published>
    <summary>Last post argued &quot;documentation is the core.&quot; So how do you manage it? I wrote a 400-line bash tool that packages AI workflows into portable skill bundles, installable into any project on demand — no Agent platform lock-in.</summary>
    <title>How to Farm Free Tokens (Part 2): Building an Agent-Independent Skills Management Workflow</title>
    <updated>2026-06-27T03:24:01.608Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="ai-agent" scheme="https://lichuanyang.top/en/tags/ai-agent/"/>
    <category term="workflow" scheme="https://lichuanyang.top/en/tags/workflow/"/>
    <category term="llm" scheme="https://lichuanyang.top/en/tags/llm/"/>
    <category term="prompt" scheme="https://lichuanyang.top/en/tags/prompt/"/>
    <category term="productivity" scheme="https://lichuanyang.top/en/tags/productivity/"/>
    <content>
      <![CDATA[<p>What’s the biggest—or perhaps only—pain point of using AI right now? Probably the bill.</p><p>Claude, Codex, Cursor, Qoder, MiMo, MiniMax—their basic plans all start around $20&#x2F;month, and you still have to watch your usage. If you want unlimited access, it gets even more expensive.</p><p>Meanwhile, lots of platforms offer free trials or permanently free models. For instance, OpenCode’s DeepSeek Flash has been free for a while.</p><p>So how do we actually make the most of these free models?</p><p>I’ve been using AI Agents across a wide range of projects—code, personal blogs, knowledge bases, fiction writing, even little games I build with AI. Over time I noticed something: <strong>every Agent operation boils down to: read your docs → assemble a prompt → send it to a model → modify files based on the result.</strong></p><p>The intelligence lives in your docs, not the platform. Whether you assemble the prompt yourself or let a platform do it—there’s really not that much difference.</p><p>So I stopped worrying about which Agent platform to use. I maintain good docs, and wherever there are free tokens, that’s where I go.</p><span id="more"></span><h2 id="What-Agents-Actually-Do"><a href="#What-Agents-Actually-Do" class="headerlink" title="What Agents Actually Do"></a>What Agents Actually Do</h2><p>Regardless of which Agent you use, the workflow is basically the same:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">You state a need → Agent reads project docs → reads relevant files → assembles a prompt → asks the model → processes the response → modifies files → loops</span><br></pre></td></tr></table></figure><p>Say you ask it to fix a bug. It will:</p><ol><li>First read your <code>AGENTS.md</code> and <code>README.md</code> to understand what the project is and where things are</li><li>Then find the files related to the bug and read the source code</li><li>Assemble “project context + relevant code + bug description + fix requirements” into a single prompt</li><li>Send it to the LLM</li><li>Turn the response into actual code changes</li></ol><p>How much difference can there really be between one Agent and another, at this level?</p><p>Don’t get me wrong—I’m not dismissing genuinely good Agents. The best ones really do handle context management, flow control, and tool integration smoothly. That’s why they’re popular. But “human intelligence” can easily bridge those gaps.</p><p>Take context management: an Agent can figure out “this bug involves the auth module, I need to read these three files”—but only because it already read your <code>AGENTS.md</code>. Those file paths, module responsibilities, and conventions? You wrote them in there yourself.</p><p>When your docs are messy, a good Agent can find information more efficiently. But with well-organized docs, the differences between Agents become negligible.</p><p>Bottom line: good Agents are genuinely convenient in some scenarios, but they’re not worth the price. Saving a few minutes on doc maintenance and window-switching in exchange for tens or hundreds of dollars a month? Terrible ROI.</p><p>For me personally, maintaining a solid Agent guide is far more cost-effective. Besides, you can totally have AI write those guide documents for you.</p><h2 id="Documentation-Is-the-Key"><a href="#Documentation-Is-the-Key" class="headerlink" title="Documentation Is the Key"></a>Documentation Is the Key</h2><p>Once you accept the above, you have to face a counterintuitive fact: <strong>what’s truly valuable isn’t the Agent platform—it’s your own workflow.</strong></p><p>Anyone can call an LLM. Most Agent platforms are just orchestration layers with similar features. But your project docs and process management are unique—they record your project structure, design decisions, pitfalls you’ve hit, and your preferred coding style.</p><p>Same docs on a different platform? The results are basically the same. But garbage docs? No platform can save you.</p><p>Here’s how I maintain docs for code projects:</p><ul><li>Project overview doc: tech stack, directory structure, key module descriptions</li><li>Workflow docs: “how to add a feature”, “bug fix steps”, “release process”</li><li>Rule docs: naming conventions, comment requirements, lessons learned</li></ul><p>Same project directory, opened with different Agents—barely any difference in results.</p><p>Same goes for fiction projects. World-building, character profiles, chapter outlines, writing guidelines—keep these docs consistent, and you’ll get consistent style across different models. <strong>The docs set the shape; the content quality follows.</strong></p><p>Even at the model layer, the differences aren’t that big. DeepSeek V4 Flash handles the vast majority of tasks just fine.</p><p>If you really want to spend money to save time, invest in the documentation itself—not in choosing the “right” platform.</p><h2 id="Token-Farming-in-Practice"><a href="#Token-Farming-in-Practice" class="headerlink" title="Token Farming in Practice"></a>Token Farming in Practice</h2><p>Once your workflow lives in docs, the biggest benefit kicks in: you can switch platforms freely.</p><p>Whichever platform has free tokens, use it. When it runs out, move to the next.</p><p>Here are some I use:</p><ul><li><strong>OpenCode</strong>: Free DeepSeek Flash daily</li><li><strong>Qoder</strong>: Some trial credits for new users, plus free model quotas</li><li><strong>Codex</strong>: Trial bundle—honestly, I still haven’t gotten around to using it. I keep thinking I’ll save the free quota for a genuinely complex task, but those tasks never actually come up.</li><li><strong>Hermes</strong>: Occasionally releases free models. Sometimes you luck into a great one—like when they had free DeepSeek Flash a while back.</li><li><strong>Trae</strong>: Completely free, just need to queue. When a task really feels too much for the free models, I hop over to Trae temporarily. Works perfectly fine.</li></ul><p>Other avenues worth exploring:</p><ul><li><strong>LLM API new-user credits</strong>: OpenAI, Anthropic, DeepSeek, etc. New accounts usually come with a few dollars. Calling the API directly is cheaper than going through an Agent platform.</li><li><strong>Free hosting for open-source models</strong>: HuggingFace, Together AI offer free inference quotas. More than enough for small tasks.</li><li><strong>Student discounts</strong>: GitHub Student Pack and the like come with a bunch of platform credits.</li><li><strong>Community events</strong>: AI platforms often give away credits through events.</li></ul><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>The takeaway is simple: <strong>Agents are just tools. Documentation is the core.</strong> Maintain your docs well, and you can switch platforms at will, farming free tokens wherever they pop up. Saving tens of dollars a month—that’s a nice hotpot dinner right there.</p><p>As for those burning through hundreds of dollars in tokens every month—hey, your money is well spent. But my tokens cost nothing, and the results are about the same.</p><p>Source: <a href="https://lichuanyang.top/en/posts/26060/">https://lichuanyang.top/en/posts/26060/</a></p><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-What’s-the-relationship-between-Agents-and-Skills"><a href="#Q-What’s-the-relationship-between-Agents-and-Skills" class="headerlink" title="Q: What’s the relationship between Agents and Skills?"></a>Q: What’s the relationship between Agents and Skills?</h3><p>An Agent is the “worker” executing tasks; a Skill is the “instruction manual” telling it how. The Agent’s underlying model provides general reasoning ability, but it doesn’t know your project structure, coding conventions, or API design constraints — that knowledge must be communicated through Skills (structured instruction documents). The relationship can be understood as: Agent &#x3D; General Reasoning Engine + On-Demand Skill Loading. If you maintain your Skills well, you can switch between different Agent platforms and still get consistent output quality.</p><h3 id="Q-What’s-the-most-effective-way-to-optimize-token-usage"><a href="#Q-What’s-the-most-effective-way-to-optimize-token-usage" class="headerlink" title="Q: What’s the most effective way to optimize token usage?"></a>Q: What’s the most effective way to optimize token usage?</h3><p>Not switching to cheaper models — it’s <strong>writing better docs</strong>. A well-crafted AGENTS.md or SKILL.md allows the Agent to precisely locate the files it needs, avoiding a full project scan for every task. In comparison, tweaking model choices or prompt phrasing on top of poor documentation saves far fewer tokens than good documentation would. Invest time in clear docs first, then farm free tokens — that’s the highest-ROI path.</p><h3 id="Q-Which-is-a-better-investment-—-maintaining-docs-or-subscribing-to-an-Agent"><a href="#Q-Which-is-a-better-investment-—-maintaining-docs-or-subscribing-to-an-Agent" class="headerlink" title="Q: Which is a better investment — maintaining docs or subscribing to an Agent?"></a>Q: Which is a better investment — maintaining docs or subscribing to an Agent?</h3><p>Maintaining docs is the better investment. An Agent subscription costs tens to hundreds of dollars a month, essentially paying for the platform’s convenience of “finding files, assembling prompts.” But if your project docs are already comprehensive — tech stack, directory structure, design decisions, coding conventions all spelled out — switching to a free Agent yields almost identical results. Docs are your asset, reusable across platforms. An Agent subscription is a consumable — switch platforms and you start over.</p><h3 id="Q-Are-free-models-really-sufficient"><a href="#Q-Are-free-models-really-sufficient" class="headerlink" title="Q: Are free models really sufficient?"></a>Q: Are free models really sufficient?</h3><p>For the vast majority of daily tasks, yes. The author’s experience shows that DeepSeek Flash-level free models can handle most code modifications, document generation, and knowledge organization tasks. When you encounter genuinely complex scenarios beyond their reach, you can temporarily switch to platforms with free trial credits (like Trae, Codex trial bundles). The real bottleneck is rarely model capability — it’s whether the context (docs) you feed it is good enough.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/26060/</id>
    <link href="https://lichuanyang.top/en/posts/26060/"/>
    <published>2026-06-03T14:00:00.000Z</published>
    <summary>After tinkering with AI Agents, I realized they're mostly just shuffling documents and prompts around. Instead of paying for subscriptions, maintain your own project docs and use whichever platform has free tokens.</summary>
    <title>How to Farm Free Tokens: Building Agent-Independent AI Workflows</title>
    <updated>2026-06-27T03:24:01.609Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="hexo" scheme="https://lichuanyang.top/en/tags/hexo/"/>
    <category term="blog" scheme="https://lichuanyang.top/en/tags/blog/"/>
    <category term="ai" scheme="https://lichuanyang.top/en/tags/ai/"/>
    <category term="ai Agent" scheme="https://lichuanyang.top/en/tags/ai-Agent/"/>
    <category term="automation" scheme="https://lichuanyang.top/en/tags/automation/"/>
    <category term="llm" scheme="https://lichuanyang.top/en/tags/llm/"/>
    <category term="developer-tools" scheme="https://lichuanyang.top/en/tags/developer-tools/"/>
    <category term="prompt-engineering" scheme="https://lichuanyang.top/en/tags/prompt-engineering/"/>
    <content>
      <![CDATA[<h2 id="The-Problem-A-Tedious-Task-No-One-Wants-to-Do"><a href="#The-Problem-A-Tedious-Task-No-One-Wants-to-Do" class="headerlink" title="The Problem: A Tedious Task No One Wants to Do"></a>The Problem: A Tedious Task No One Wants to Do</h2><p>Recently, I decided to migrate my Hexo blog from the Next theme to Butterfly. Sounds simple, right?</p><p>If you’ve ever done a Hexo theme migration, you know it’s anything but simple:</p><ol><li><strong>Config files are 1000+ lines long</strong>, requiring line-by-line comparison between old and new theme formats</li><li><strong>Feature mapping is complex</strong>: Next’s <code>leancloud_visitors</code> maps to Butterfly’s <code>busuanzi</code>, Next’s <code>reading_progress</code> maps to Butterfly’s <code>preloader</code></li><li><strong>Two sites need syncing</strong>: Both Chinese and English sites need updates, with slightly different configs</li><li><strong>Font Awesome version conflicts</strong>: Butterfly defaults to FA 7.1.0, which doesn’t exist on cdnjs</li><li><strong>YAML format sensitivity</strong>: Indentation and line endings can break everything</li></ol><p>Manually, this would take an entire day and be error-prone. So I tried something different: <strong>letting an AI Agent handle the entire task</strong>.</p><h2 id="The-Solution-AI-Agent-Driven-Migration"><a href="#The-Solution-AI-Agent-Driven-Migration" class="headerlink" title="The Solution: AI Agent-Driven Migration"></a>The Solution: AI Agent-Driven Migration</h2><p>I used <strong>Hermes Agent</strong>, an AI-powered development assistant, to automate the entire migration process.</p><h3 id="What-the-AI-Agent-Did"><a href="#What-the-AI-Agent-Did" class="headerlink" title="What the AI Agent Did"></a>What the AI Agent Did</h3><p>The AI Agent autonomously completed these steps:</p><ol><li><strong>Environment Analysis</strong>: Checked current Hexo version (7.0.0), Node.js version (v24.14.0), and config differences between two sites</li><li><strong>Version Research</strong>: Queried GitHub API for the latest Hexo version (8.1.2) and breaking changes</li><li><strong>Dependency Upgrade</strong>: Batch-updated hexo and all plugins to latest versions</li><li><strong>Theme Comparison</strong>: Analyzed feature coverage between Next and Butterfly, generating a comparison table</li><li><strong>Config Migration</strong>: Mapped 1000+ lines of Next config to Butterfly format</li><li><strong>Bug Detection</strong>:<ul><li>Discovered FA 7.1.0 returns 404 on cdnjs, downgraded to 6.7.2</li><li>Found Zhihu icon needs <code>fab</code> prefix (brand icon)</li><li>Detected English site menu path error</li><li>Recovered lost GA, Baidu Analytics, and site verification configs</li></ul></li><li><strong>Testing</strong>: Build tests, local preview server, HTML output verification</li><li><strong>Code Commits</strong>: Auto-generated commit messages and pushed to GitHub</li></ol><h3 id="Key-Technical-Insights"><a href="#Key-Technical-Insights" class="headerlink" title="Key Technical Insights"></a>Key Technical Insights</h3><h4 id="1-Font-Awesome-Version-Compatibility"><a href="#1-Font-Awesome-Version-Compatibility" class="headerlink" title="1. Font Awesome Version Compatibility"></a>1. Font Awesome Version Compatibility</h4><p>This was the most subtle bug. Butterfly’s <code>plugins.yml</code> configured FA 7.1.0:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">fontawesome:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">&#x27;@fortawesome/fontawesome-free&#x27;</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">7.1</span><span class="number">.0</span></span><br></pre></td></tr></table></figure><p>But this version doesn’t exist on cdnjs! All brand icons (GitHub, StackOverflow, Zhihu) failed to load.</p><p>The AI Agent diagnosed this by:</p><ul><li>Inspecting generated HTML, finding FA 7.1.0 reference</li><li>Querying cdnjs API, confirming 404 for version 7.1.0</li><li>Testing FA 6.7.2, verifying all required icons exist</li><li>Overriding CDN address in config</li></ul><h4 id="2-YAML-Config-Migration-Pitfalls"><a href="#2-YAML-Config-Migration-Pitfalls" class="headerlink" title="2. YAML Config Migration Pitfalls"></a>2. YAML Config Migration Pitfalls</h4><p>Hexo config files use YAML format, which is sensitive to indentation and line endings:</p><ul><li><strong>CRLF vs LF</strong>: Butterfly’s default config uses CRLF line endings, causing sed pattern matching to fail</li><li><strong>Duplicate keys</strong>: The patch tool created duplicate <code>scroll_percent</code> entries</li><li><strong>Lost values</strong>: Incorrect YAML indentation caused config values to not be written</li></ul><p>These issues were resolved by using Python to directly manipulate file content.</p><h4 id="3-Multi-Site-Synchronization"><a href="#3-Multi-Site-Synchronization" class="headerlink" title="3. Multi-Site Synchronization"></a>3. Multi-Site Synchronization</h4><p>The Chinese and English sites share most config, but have key differences:</p><ul><li>English site needs <code>language: en</code></li><li>English site menu needs to point to Chinese site</li><li>English site Valine placeholder needs English text</li></ul><p>The AI Agent automatically identified and handled these differences.</p><h2 id="Results-Comparison"><a href="#Results-Comparison" class="headerlink" title="Results Comparison"></a>Results Comparison</h2><table><thead><tr><th>Metric</th><th>Manual</th><th>AI Agent</th></tr></thead><tbody><tr><td>Time</td><td>4-8 hours</td><td>30 minutes</td></tr><tr><td>Error Rate</td><td>High (easy to miss configs)</td><td>Low (systematic checking)</td></tr><tr><td>Debugging</td><td>Manual Google searches</td><td>Auto root cause analysis</td></tr><tr><td>Commits</td><td>Manual commit messages</td><td>Auto-generated</td></tr></tbody></table><h2 id="Why-AI-Agents-Excel-at-This"><a href="#Why-AI-Agents-Excel-at-This" class="headerlink" title="Why AI Agents Excel at This"></a>Why AI Agents Excel at This</h2><p>This experience showed me why <strong>AI Agents are perfect for tedious, repetitive work</strong>:</p><h3 id="1-Systematic-Thinking"><a href="#1-Systematic-Thinking" class="headerlink" title="1. Systematic Thinking"></a>1. Systematic Thinking</h3><p>AI Agents don’t work like humans who “fix things as they see them.” They:</p><ul><li>Analyze current state first</li><li>Create a complete plan</li><li>Execute step by step with verification</li><li>Fix issues as they arise</li></ul><h3 id="2-Cross-Domain-Knowledge"><a href="#2-Cross-Domain-Knowledge" class="headerlink" title="2. Cross-Domain Knowledge"></a>2. Cross-Domain Knowledge</h3><p>Theme migration spans multiple technical domains:</p><ul><li>Hexo configuration</li><li>Font Awesome version management</li><li>YAML format handling</li><li>Git workflows</li><li>Frontend resource loading</li></ul><p>AI Agents can freely switch between these domains, while human developers typically only master one or two.</p><h3 id="3-Persistent-Attention"><a href="#3-Persistent-Attention" class="headerlink" title="3. Persistent Attention"></a>3. Persistent Attention</h3><p>Humans lose focus when processing 1000+ line config files, leading to oversights. AI Agents don’t get tired—every config item gets checked.</p><h3 id="4-Automated-Verification"><a href="#4-Automated-Verification" class="headerlink" title="4. Automated Verification"></a>4. Automated Verification</h3><p>AI Agents don’t just modify configs—they also:</p><ul><li>Run build tests</li><li>Start local servers for verification</li><li>Check HTML output</li><li>Confirm critical configs are active</li></ul><h2 id="Best-Use-Cases"><a href="#Best-Use-Cases" class="headerlink" title="Best Use Cases"></a>Best Use Cases</h2><p>Based on this experience, <strong>AI Agents are ideal for</strong>:</p><ol><li><strong>Config Migration</strong>: Converting configs between different systems</li><li><strong>Dependency Upgrades</strong>: Batch-updating multiple packages with compatibility handling</li><li><strong>Code Refactoring</strong>: Large-scale code format adjustments</li><li><strong>Documentation</strong>: Integrating information from multiple sources</li><li><strong>Environment Setup</strong>: New project initialization</li></ol><h2 id="Limitations"><a href="#Limitations" class="headerlink" title="Limitations"></a>Limitations</h2><p>AI Agents have limitations too:</p><ol><li><strong>Creative Work</strong>: UI design, copywriting still need human direction</li><li><strong>Business Decisions</strong>: Whether to upgrade, which theme to choose—these need human judgment</li><li><strong>Complex Debugging</strong>: Some runtime issues require human intervention</li></ol><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>This practice of using an AI Agent to complete Hexo theme migration made me see the huge potential of <strong>LLMs (Large Language Models) in software engineering</strong>.</p><p>AI Agents don’t replace developers—they enhance our capabilities. They free us from tedious, repetitive work so we can focus on more valuable things.</p><p>If you have similar “grunt work,” try letting an AI Agent help. You’ll find that <strong>AI-assisted development</strong> is not the future—it’s the present.</p><h2 id="Tech-Stack"><a href="#Tech-Stack" class="headerlink" title="Tech Stack"></a>Tech Stack</h2><ul><li><strong>AI Agent</strong>: Hermes Agent</li><li><strong>LLM</strong>: DeepSeek V4 Pro &#x2F; MiMo v2.5</li><li><strong>Static Site Generator</strong>: Hexo 8.1.2</li><li><strong>Theme</strong>: Butterfly 5.5.4</li><li><strong>Icon Library</strong>: Font Awesome 6.7.2</li><li><strong>Comment System</strong>: Valine (LeanCloud)</li><li><strong>Analytics</strong>: Google Analytics &#x2F; Baidu Analytics</li></ul><hr><p><em>This article was written with AI Agent assistance, documenting a real theme migration practice.</em></p><p>Source: <a href="https://lichuanyang.top/en/posts/48979/">https://lichuanyang.top/en/posts/48979/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/48979/</id>
    <link href="https://lichuanyang.top/en/posts/48979/"/>
    <published>2026-05-28T14:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="The-Problem-A-Tedious-Task-No-One-Wants-to-Do"><a href="#The-Problem-A-Tedious-Task-No-One-Wants-to-Do" class="headerlink"]]>
    </summary>
    <title>Automating Hexo Theme Migration with AI Agent: From Next to Butterfly</title>
    <updated>2026-06-27T03:23:24.830Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="blog" scheme="https://lichuanyang.top/en/tags/blog/"/>
    <category term="vercel" scheme="https://lichuanyang.top/en/tags/vercel/"/>
    <category term="devops" scheme="https://lichuanyang.top/en/tags/devops/"/>
    <category term="independent-blog" scheme="https://lichuanyang.top/en/tags/independent-blog/"/>
    <content>
      <![CDATA[<p>A few days ago, I wrote a new article and deployed it to Vercel as usual, but the pipeline threw an error. At first I thought it was a build issue, and after messing around for a while I realized something was wrong — it was the Vercel account itself. The account registered with a 163 email couldn’t log in at all. Later I found out that Vercel had banned the entire 163.com email root domain.</p><span id="more"></span><h2 id="What-is-Vercel"><a href="#What-is-Vercel" class="headerlink" title="What is Vercel?"></a>What is Vercel?</h2><p>If you’re into independent blogging, you’ve probably heard of or are currently using Vercel. Simply put, it’s a frontend deployment platform that supports automatically pulling code from GitHub repositories, building, and deploying. It assigns a domain name to your website and includes CDN acceleration. Many people’s blogs use the GitHub Pages + Vercel combination — code on GitHub, using Vercel for deployment and hosting.</p><p>My own blog was set up this way. GitHub Pages can deploy static pages directly, but Vercel is more convenient for custom domains, HTTPS, and CDN, so I chose it.</p><h2 id="The-Full-Story-of-the-163-Email-Ban"><a href="#The-Full-Story-of-the-163-Email-Ban" class="headerlink" title="The Full Story of the 163 Email Ban"></a>The Full Story of the 163 Email Ban</h2><p>Around May 4, 2026, a large number of users who registered Vercel with 163 email suddenly found they couldn’t log into their accounts. I searched online and found it wasn’t just me — forums like NodeSeek and V2EX had people discussing the same issue.</p><p>According to online information, Vercel suffered a relatively serious security incident in mid-April 2026. The hacking group ShinyHunters breached Vercel’s internal systems through a third-party AI tool, stealing employee data, enterprise backend access, and some deployment credentials, and even demanded a $2 million ransom.</p><p>The 163 email ban was likely one of the security measures taken in response. It may be because 163 email was extensively used in this incident for malicious registrations or attacks, or it may just be Vercel applying stricter risk controls to certain email domains. Vercel hasn’t publicly stated the specific reason, but the result was that the entire 163.com email domain was blanket banned.</p><p>The impact was significant. Many domestic developers and independent blog authors use 163 email to register Vercel, and this ban directly made all their blogs inaccessible.</p><p>However, if your Vercel account is still logged in (for example, your work computer’s browser maintains the login session), you can actually salvage it: go to account settings, add a new email (Outlook or Gmail both work), and then remove the 163 email. This way the account can continue to be used normally without redeployment. Someone online shared this method, which can save a lot of trouble. But if you’re like me and by the time you discovered it, you were completely locked out, then you’ll have to follow the rebuild process below.</p><h2 id="Recalling-My-Blog-Deployment-Architecture"><a href="#Recalling-My-Blog-Deployment-Architecture" class="headerlink" title="Recalling My Blog Deployment Architecture"></a>Recalling My Blog Deployment Architecture</h2><p>The account was banned, the blog was inaccessible. The first thing I needed to do was figure out exactly how my blog was deployed. Honestly, the blog configuration was set up a long time ago, and I hadn’t touched it much since. I couldn’t remember the exact process very well.</p><p>It took considerable effort to recall. The general pipeline was:</p><ol><li>Blog source code is in a GitHub repository</li><li>Vercel connects to the GitHub repository, responsible for building and deploying</li><li>Domain name resolution was split into two layers: Alibaba Cloud DNS points to Cloudflare, Cloudflare then points to Vercel</li></ol><p>Why make it so complicated? Mainly to use Cloudflare’s CDN and security capabilities. Alibaba Cloud as the domain registrar, Cloudflare as the intermediate layer for DNS management and CDN, Vercel as the final static page host. When I configured it, I was following some online tutorials. Although the chain was a bit long, it ran stably for a long time, so I never changed it.</p><h2 id="Recovery-Process-New-Account-Redeployment"><a href="#Recovery-Process-New-Account-Redeployment" class="headerlink" title="Recovery Process: New Account + Redeployment"></a>Recovery Process: New Account + Redeployment</h2><p>Once I figured out the deployment architecture, the recovery process was actually not complicated.</p><p><strong>Step 1: Register a new Vercel account</strong></p><p>I learned my lesson this time and didn’t use 163 email. I registered a new Vercel account with Outlook. Gmail would also work, but considering this was Vercel banning specific email domains, using a mainstream international email service would be more reliable.</p><p><strong>Step 2: Redeploy the GitHub project</strong></p><p>After logging into the new account, I connected GitHub and imported the blog repository into Vercel. Vercel automatically detected the project type, and the build and deployment were completed automatically, basically the same as before.</p><p><strong>Step 3: Reconfigure the domain</strong></p><p>This was the key step. Since the previous domain resolution followed the Alibaba Cloud → Cloudflare → Vercel chain, I needed to bind the domain to the new Vercel project after redeployment.</p><p>What pleasantly surprised me was that Vercel now supports automatically modifying Cloudflare DNS configuration when adding a custom domain. I remember previously having to manually go to Cloudflare’s backend to add CNAME records. Now Vercel handles it directly through API — just a few clicks and it’s done.</p><p>The entire recovery process, from registering a new account to having the blog accessible again, took less than half an hour.</p><h2 id="Lessons-Learned"><a href="#Lessons-Learned" class="headerlink" title="Lessons Learned"></a>Lessons Learned</h2><ol><li><p><strong>Don’t use domestic email to register overseas services.</strong> Domestic email services like 163 and QQ mail are inherently high-risk targets in overseas services’ risk control systems. This time it was Vercel; next time it could be another service. It’s recommended to use Gmail or Outlook for primary accounts.</p></li><li><p><strong>Keep good records of your deployment architecture.</strong> The most time-consuming part of this incident was recalling the deployment pipeline. I thought I remembered it all after configuring it, but after so long I had mostly forgotten. I recommend writing down your deployment architecture and key configurations in notes, even if it’s just a few sentences.</p></li><li><p><strong>Vercel’s Cloudflare integration is now quite good.</strong> Previously when configuring DNS manually, it was error-prone. Now with much higher automation, the cost of redeployment has been significantly reduced.</p></li><li><p><strong>If possible, prepare backups and multi-platform options.</strong> I was lucky this time and recovery was relatively quick. But if the GitHub repository also had issues, or if Vercel completely banned the project rather than just the account, recovery would be much more troublesome. For important blog content, it’s recommended to have a copy locally or on another platform.</p></li></ol><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>This incident of Vercel banning 163 email serves as a wake-up call for everyone who uses domestic email to register overseas services. We can’t control the platform’s security measures, but we can prepare in advance for our own account security and deployment architecture.</p><p>If you’ve also encountered the issue of Vercel 163 email being banned or Vercel account login failure, I hope this article helps. The core recovery approach is: register a new account with a different email, redeploy the project, and rebind the domain. The operations aren’t complicated, but the prerequisite is that you need to remember your deployment pipeline.</p><p>Original article: <a href="https://lichuanyang.top/posts/39648/">https://lichuanyang.top/posts/39648/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/39648/">https://lichuanyang.top/en/posts/39648/</a></p><hr><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><h3 id="Step-1-Identify-the-Problem"><a href="#Step-1-Identify-the-Problem" class="headerlink" title="Step 1: Identify the Problem"></a>Step 1: Identify the Problem</h3><p>When your blog is inaccessible or Vercel deployment throws errors, first confirm whether your account has been banned. Try logging into Vercel — if your 163 email-registered account cannot log in, it’s likely been banned.</p><h3 id="Step-2-Investigate-the-Cause"><a href="#Step-2-Investigate-the-Cause" class="headerlink" title="Step 2: Investigate the Cause"></a>Step 2: Investigate the Cause</h3><p>Search related forums (NodeSeek, V2EX, etc.) to confirm whether Vercel has banned 163 email on a large scale. Understand the background and impact scope of the ban to be well-informed.</p><h3 id="Step-3-Migrate-Email-Verification"><a href="#Step-3-Migrate-Email-Verification" class="headerlink" title="Step 3: Migrate Email Verification"></a>Step 3: Migrate Email Verification</h3><p>If you can still log in, immediately add a Gmail or Outlook email in your account settings and remove the 163 email. If you can no longer log in, re-register a Vercel account with a new email. It’s recommended to use mainstream international email services like Gmail or Outlook.</p><h3 id="Step-4-Redeploy"><a href="#Step-4-Redeploy" class="headerlink" title="Step 4: Redeploy"></a>Step 4: Redeploy</h3><p>After logging in with a new account, connect to GitHub and import your blog repository. Vercel will automatically detect the project type and complete building and deploying — the process is essentially the same as the initial deployment.</p><h3 id="Step-5-Verify-Recovery"><a href="#Step-5-Verify-Recovery" class="headerlink" title="Step 5: Verify Recovery"></a>Step 5: Verify Recovery</h3><p>Re-bind your custom domain. Vercel now supports automatically modifying Cloudflare DNS configuration, making the process very simple. Confirm that all pages of your blog are accessible and that HTTPS, CDN, and other functions are working properly.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/39648/</id>
    <link href="https://lichuanyang.top/en/posts/39648/"/>
    <published>2026-05-11T11:30:00.000Z</published>
    <summary>A firsthand account of Vercel banning 163 email causing blog deployment failure, and a complete solution from discovering the issue to migrating and recovering.</summary>
    <title>How I Recovered My Blog After Vercel Banned 163 Email</title>
    <updated>2026-06-27T03:50:00.010Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="AI Practice" scheme="https://lichuanyang.top/en/categories/AI-Practice/"/>
    <category term="knowledge-management" scheme="https://lichuanyang.top/en/tags/knowledge-management/"/>
    <category term="llm" scheme="https://lichuanyang.top/en/tags/llm/"/>
    <category term="security-standards" scheme="https://lichuanyang.top/en/tags/security-standards/"/>
    <category term="wiki" scheme="https://lichuanyang.top/en/tags/wiki/"/>
    <category term="practice" scheme="https://lichuanyang.top/en/tags/practice/"/>
    <content>
      <![CDATA[<p>When I was organizing our team’s security development standards recently, I encountered an old problem: security documentation keeps piling up, but when you actually need it, you can’t find it. Every time a new team member joins, they have to dig through various documents to piece together the complete security standards. Every time we have a security incident, the lessons learned are scattered everywhere, and we end up rediscovering the same issues next time.</p><p>I’ve tried managing this with Confluence, Notion, even Git repository READMEs, but none worked well. It wasn’t until I saw Karpathy’s llm-wiki concept that I thought this might be a breakthrough approach.</p><span id="more"></span><h2 id="What-is-Karpathy’s-llm-wiki"><a href="#What-is-Karpathy’s-llm-wiki" class="headerlink" title="What is Karpathy’s llm-wiki?"></a>What is Karpathy’s llm-wiki?</h2><p>Karpathy proposed an idea in his <a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">Gist</a>: let LLMs maintain a Wiki middleware layer. His exact words were “the wiki is a persistent, compounding artifact” - this Wiki layer acts like a compiler, compiling raw documents into structured data. This way, when the LLM answers questions, it doesn’t need to search through raw documents from scratch; it can directly query the Wiki.</p><p>I think this approach is brilliant. Traditional document management is about “storing” and “finding”, while llm-wiki is about “compiling” and “querying”. Raw documents might be messy, duplicated, or even contradictory, but the Wiki layer is clean, structured, and cross-referenced.</p><p>For example: our team has over a dozen security development standard documents, covering everything from input validation to penetration testing, plus nearly ten security incident post-mortem reports. These documents come in different formats - some in Word, some in Markdown, some are even just Slack chat exports. If we let the LLM process these raw materials directly, efficiency would be very low. But if we first “compile” them into a Wiki, the results are completely different.</p><h2 id="My-Implementation-Process"><a href="#My-Implementation-Process" class="headerlink" title="My Implementation Process"></a>My Implementation Process</h2><h3 id="Step-1-Generate-CLAUDE-md"><a href="#Step-1-Generate-CLAUDE-md" class="headerlink" title="Step 1: Generate CLAUDE.md"></a>Step 1: Generate CLAUDE.md</h3><p>I first gave Claude Karpathy’s Gist link and asked it to generate a CLAUDE.md file based on this concept. This file serves as an “operation manual” for the LLM, telling it how to maintain the Wiki.</p><p>The generated CLAUDE.md contained several key sections:</p><ul><li>Three-layer architecture explanation (raw → wiki → output)</li><li>Wiki page specification (frontmatter format, cross-reference rules)</li><li>Ingestion workflow (how to process new materials)</li><li>Query workflow (how to answer user questions)</li><li>Lint checks (how to maintain Wiki health)</li></ul><p>In practice, I found the model’s output was basically usable without much fine-tuning. I also had the AI generate the output layer for final specification documents. This completed the three-layer architecture: raw for original materials, wiki for structured Wiki, output for final output.</p><h3 id="Step-2-Collect-and-Organize-Raw-Materials"><a href="#Step-2-Collect-and-Organize-Raw-Materials" class="headerlink" title="Step 2: Collect and Organize Raw Materials"></a>Step 2: Collect and Organize Raw Materials</h3><p>This was the most time-consuming step. I collected all the scattered security development standards and incident cases, placing them uniformly in the raw&#x2F;sources&#x2F; directory. To preserve the original state, I didn’t modify the content or unify the formats - PDF, Word, PPT, Markdown, and other formats can all go directly into the raw layer; the model can handle them all.</p><p>Specifically, I collected:</p><ul><li>Over a dozen security development standard documents: covering input validation, SQL injection prevention, XSS prevention, CSRF prevention, sensitive data protection, log security, etc.</li><li>Nearly ten security incident post-mortem reports: including SQL injection vulnerabilities, unauthorized access, sensitive information leaks, session hijacking, and other real cases</li><li>Several security architecture documents: covering zero-trust architecture, secure coding guidelines, penetration testing processes, etc.</li></ul><p>The quality of these materials varied greatly. Some were well-formatted with clear headings and lists; others were rough notes, even screenshots. But that’s fine - the beauty of llm-wiki is that it can handle this kind of “dirty data”.</p><h3 id="Step-3-Let-the-LLM-Generate-the-Wiki-Layer"><a href="#Step-3-Let-the-LLM-Generate-the-Wiki-Layer" class="headerlink" title="Step 3: Let the LLM Generate the Wiki Layer"></a>Step 3: Let the LLM Generate the Wiki Layer</h3><p>With CLAUDE.md and raw materials ready, I could let the LLM start working. I used Claude here, mainly because its context window is large enough to process more content at once.</p><p>The process works like this:</p><ol><li>Use CLAUDE.md as the system prompt</li><li>Tell the LLM: “Please process all materials in the raw&#x2F;sources&#x2F; directory according to the CLAUDE.md specification and generate the wiki layer”</li><li>The LLM reads each raw file sequentially, extracts key information, and generates corresponding Wiki pages</li></ol><p>This process isn’t complete in one go. The LLM’s initial Wiki has issues:</p><ul><li>Cross-references are insufficient</li><li>Some concepts aren’t extracted into standalone pages</li><li>Logical relationships between pages aren’t clear enough</li></ul><p>So multiple iterations are needed. I review the generated Wiki, point out issues, and let the LLM fix them. For example:<br>“This incident case mentions ‘improper connection pool configuration’, but why isn’t there a corresponding concept page? Please create a [[connection-pool-best-practices]] page and reference it in the incident case.”</p><p>After 5-6 iterations, the Wiki layer gradually took shape.</p><h3 id="Step-4-Generate-Final-Specification-Documents"><a href="#Step-4-Generate-Final-Specification-Documents" class="headerlink" title="Step 4: Generate Final Specification Documents"></a>Step 4: Generate Final Specification Documents</h3><p>With the Wiki layer ready, I could generate the final specification documents. I asked the LLM to generate a developer-facing, directly readable specification document based on the Wiki layer.</p><p>Here’s a design decision: the final document goes in the output&#x2F; directory, not modifying the original materials. This maintains the clarity of the three-layer architecture:</p><ul><li>raw&#x2F;: original materials, unmodifiable</li><li>wiki&#x2F;: structured Wiki, maintained by LLM</li><li>output&#x2F;: final output, for human reading</li></ul><p>The generated specification documents have several characteristics:</p><ol><li>Each standard entry includes “why” - not just “do this”, but also “why do this”</li><li>Related incident cases are linked as “negative examples” alongside</li><li>Cross-references exist, for example “database connection pool configuration” links to the “connection pool best practices” concept page</li></ol><h2 id="Actual-Results"><a href="#Actual-Results" class="headerlink" title="Actual Results"></a>Actual Results</h2><h3 id="Reading-Experience"><a href="#Reading-Experience" class="headerlink" title="Reading Experience"></a>Reading Experience</h3><p>The final specification documents can be opened directly in Obsidian. Being in Markdown format, they support:</p><ul><li>Code block syntax highlighting</li><li>Internal link navigation (click to jump to related concepts or incident cases)</li><li>Tag filtering (e.g., filter all #security-related standards)</li></ul><p>More importantly, each security standard is linked to real incident cases. For example, next to the “SQL Injection Prevention” standard, there’s a link to the [[sql-injection-vulnerability-incident]] case, which details how the vulnerability occurred, what impact it had, and how it was fixed. This way developers not only know “what to do” but also see “what happens if you don’t do it”.</p><p>Compared to the previous scattered documents, the reading experience improved by orders of magnitude.</p><h3 id="LLM-Q-A"><a href="#LLM-Q-A" class="headerlink" title="LLM Q&amp;A"></a>LLM Q&amp;A</h3><p>Even more powerful is the LLM’s Q&amp;A capability. Because of the Wiki layer, the LLM’s answers are very accurate. For example, when I ask:<br>“What should I watch out for when doing batch database updates?”</p><p>The LLM will:</p><ol><li>First find the [[batch-operation-security]] concept page in the Wiki</li><li>Combine it with the [[sql-injection-batch-incident]] incident case</li><li>Give specific recommendations: must use parameterized queries, no string concatenation for SQL, batch operations need transaction control, large batches should be paginated, etc.</li><li>Also explain why: because if something goes wrong with batch operations, the impact is N times that of single operations</li></ol><p>Another example: “How should I handle user file uploads securely?”</p><p>The LLM will:</p><ol><li>Find the [[file-upload-security]] concept page</li><li>Combine it with the [[malicious-file-upload-incident]] incident case</li><li>Give specific recommendations: file type whitelist validation, file content detection, rename storage, limit file size, isolated storage, etc.</li></ol><p>This is much more reliable than letting the LLM “search” for answers in a dozen raw documents.</p><h3 id="Continuous-Updates"><a href="#Continuous-Updates" class="headerlink" title="Continuous Updates"></a>Continuous Updates</h3><p>What satisfied me most is the update mechanism. When new standard documents or incident reports arrive, I just need to:</p><ol><li>Place the new file in the raw&#x2F;sources&#x2F; directory</li><li>Tell the LLM: “There are new materials to ingest”</li><li>The LLM automatically updates the Wiki layer and specification documents</li></ol><p>For example, last week we had a new security incident: an XSS vulnerability due to unfiltered user input. After I placed the incident report in the raw directory, the LLM automatically:</p><ol><li>Created the [[xss-user-input-incident]] incident case page</li><li>Updated the [[input-validation-security]] concept page with XSS-related notes</li><li>Added relevant entries to the “Input Validation Standards” section of the specification document</li></ol><h2 id="Problems-Encountered-and-Solutions"><a href="#Problems-Encountered-and-Solutions" class="headerlink" title="Problems Encountered and Solutions"></a>Problems Encountered and Solutions</h2><h3 id="Problem-1-Unstable-Wiki-Quality-from-LLM"><a href="#Problem-1-Unstable-Wiki-Quality-from-LLM" class="headerlink" title="Problem 1: Unstable Wiki Quality from LLM"></a>Problem 1: Unstable Wiki Quality from LLM</h3><p>At first, the LLM-generated Wiki quality varied greatly. Sometimes it extracted key information well, sometimes it missed important content.</p><p><strong>Solution</strong>: I refined the Wiki page specification in CLAUDE.md. For example, I required each page to have:</p><ul><li>Summary line: one sentence describing the core content</li><li>Tags line: mark topics with #tags</li><li>Clear page type (concept&#x2F;entity&#x2F;source&#x2F;comparison)</li></ul><p>For security incident cases, I also required: vulnerability type, attack vector, impact scope, fix solution, prevention measures. This gave the LLM clearer guidance, and generation quality became much more stable.</p><h3 id="Problem-2-Cross-References-Frequently-Break"><a href="#Problem-2-Cross-References-Frequently-Break" class="headerlink" title="Problem 2: Cross-References Frequently Break"></a>Problem 2: Cross-References Frequently Break</h3><p>The core value of Wiki lies in cross-references, but the LLM often creates one-way links or links to non-existent pages.</p><p><strong>Solution</strong>: I strengthened cross-reference rules in CLAUDE.md:</p><ol><li>Must be bidirectional: if page A references [[B]], then page B must also reference [[A]]</li><li>The related field in frontmatter must match the [[wiki-links]] in the body</li><li>Can only reference existing pages; must create before referencing</li></ol><p>I also added Lint checks to regularly check for broken links and orphan pages.</p><h3 id="Problem-3-Low-Efficiency-Processing-Unstructured-Materials"><a href="#Problem-3-Low-Efficiency-Processing-Unstructured-Materials" class="headerlink" title="Problem 3: Low Efficiency Processing Unstructured Materials"></a>Problem 3: Low Efficiency Processing Unstructured Materials</h3><p>Some raw materials are poor quality, like screenshots, handwritten notes, or even Slack chat exports. The LLM processes these materials inefficiently.</p><p><strong>Solution</strong>: For these materials, I first manually organize them into structured Markdown, then hand them to the LLM. Although this increases upfront workload, it ensures final quality.</p><h2 id="Lessons-Learned"><a href="#Lessons-Learned" class="headerlink" title="Lessons Learned"></a>Lessons Learned</h2><ol><li><p><strong>Don’t aim for perfection in one shot</strong>. llm-wiki is an iterative process. The first version of the Wiki won’t be perfect, but it can be improved through continuous iteration.</p></li><li><p><strong>Keep optimizing CLAUDE.md</strong>. As you use it, you’ll discover missing rules in CLAUDE.md. For example, I later added structural requirements like “security incident cases must include: vulnerability type, attack vector, impact scope, fix solution, prevention measures.”</p></li><li><p><strong>Keep the three-layer architecture pure</strong>. raw&#x2F; is raw&#x2F;, don’t modify it; wiki&#x2F; is maintained by LLM; output&#x2F; is for humans. Mixing them together will only get messier.</p></li><li><p><strong>Do regular health checks</strong>. I run Lint weekly to check for broken links, orphan pages, one-way links, etc. Fix problems as soon as they’re found.</p></li><li><p><strong>Accept imperfection</strong>. The LLM-generated Wiki won’t be 100% accurate; there will be omissions and errors. But compared to completely manual maintenance, it’s already much better.</p></li></ol><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Using llm-wiki to manage security development standards essentially transforms “document management” into “knowledge engineering”. Raw documents are “raw materials”, Wiki is “semi-finished products”, and specification documents are “finished products”. The LLM serves as both “compiler” and “maintainer” in this process.</p><p>The greatest value of this approach is <strong>sustainability</strong>. Traditional document management eventually gets abandoned because maintenance costs are too high. llm-wiki shifts maintenance costs to the LLM, and humans only need to:</p><ol><li>Provide raw materials (this can’t be avoided)</li><li>Review and correct the LLM’s work (much easier than writing from scratch)</li><li>Use the final output (enjoy the results)</li></ol><p>If you’re also struggling with managing security development standards, give this approach a try. You don’t need to copy my implementation exactly; the key is understanding the core concept of “Wiki middleware layer”. As for which tools to use or how to organize directories, you can adjust according to your actual situation.</p><p>After all, tools exist to serve people, don’t they?</p><p>Source: <a href="https://lichuanyang.top/en/posts/16495/">https://lichuanyang.top/en/posts/16495/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/64/</id>
    <link href="https://lichuanyang.top/en/posts/64/"/>
    <published>2026-05-11T07:38:16.000Z</published>
    <summary>How to use LLM to manage team security development standards, based on Karpathy's llm-wiki approach to solve scattered documentation problems.</summary>
    <title>Using LLM to Manage Security Development Standards - An llm-wiki Practice</title>
    <updated>2026-06-27T03:48:13.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="vaadin" scheme="https://lichuanyang.top/en/tags/vaadin/"/>
    <category term="springboot" scheme="https://lichuanyang.top/en/tags/springboot/"/>
    <category term="frontend-development" scheme="https://lichuanyang.top/en/tags/frontend-development/"/>
    <content>
      <![CDATA[<h2 id="The-Frontend-Dilemma-for-Backend-Engineers"><a href="#The-Frontend-Dilemma-for-Backend-Engineers" class="headerlink" title="The Frontend Dilemma for Backend Engineers"></a>The Frontend Dilemma for Backend Engineers</h2><p>The pain point of backend engineers developing frontend code is usually that it’s too tedious — you often have to spend a long time looking things up for even the smallest task. Vaadin solves this pain point well by providing backend engineers with an easy-to-learn, convenient solution for writing frontend code. Today, let’s take a look.</p><span id="more"></span><h2 id="What-is-Vaadin"><a href="#What-is-Vaadin" class="headerlink" title="What is Vaadin"></a>What is Vaadin</h2><p>Hello everyone, today I’d like to introduce a tool that’s especially valuable for backend engineers — Vaadin.</p><p>To be honest, getting started with basic HTML and CSS development isn’t really difficult. But if you only know these basics, development can be very tedious. If you want to use the various tools in the frontend ecosystem, convenience improves but so does the learning curve. So unless you’re a professional full-stack engineer, just a backend developer wanting to write some frontend code temporarily — for example, when building a small project on your own — it’s usually a painful process.</p><p>Vaadin solves this pain point well. Through Vaadin’s pre-packaged common frontend components, we can write functional, reasonably presentable pages with nearly zero learning cost. For programmers with a backend background, this will significantly reduce the cost of building small projects on your own.</p><h2 id="Core-Concepts"><a href="#Core-Concepts" class="headerlink" title="Core Concepts"></a>Core Concepts</h2><p>What Vaadin offers is the ability to write pages directly in Java code. Vaadin provides various pre-built frontend styles including input fields, forms, and more, and it deeply integrates with Spring Boot, making it very convenient to use.</p><p>The underlying principle of Vaadin isn’t complex — it’s mainly based on server-side rendering, meaning the final HTML code is generated on the backend and sent to the browser. Server-side rendering isn’t uncommon, and we won’t discuss its advantages and disadvantages compared to client-side rendering here. Of course, for Vaadin, using server-side rendering makes perfect sense — since you’re writing backend code, doing rendering on the backend is a completely natural implementation path. Vaadin’s engine encapsulates the interaction between frontend and backend, so for users, the interaction between frontend and backend is transparent. At the page level, we can also call backend services normally.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><p>Here’s a code example I wrote to give you a more intuitive sense of Vaadin’s capabilities:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">@Route(value = &quot;path&quot;, layout = MainView.class)</span><br><span class="line">@PageTitle(&quot;路径规划&quot;)</span><br><span class="line">public class PathView extends VerticalLayout &#123;</span><br><span class="line"></span><br><span class="line">    @Autowired</span><br><span class="line">    private PathService pathService;</span><br><span class="line"></span><br><span class="line">    public PathView() &#123;</span><br><span class="line">        TextField start = new TextField();</span><br><span class="line">        TextField end = new TextField();</span><br><span class="line">        HorizontalLayout path = new HorizontalLayout();</span><br><span class="line">        path.add(start, end);</span><br><span class="line">        Button pathCalculate = new Button(&quot;calculate path&quot;);</span><br><span class="line"></span><br><span class="line">        VerticalLayout result = new VerticalLayout();</span><br><span class="line">        TextField transferNum = new TextField();</span><br><span class="line">        TextField distance = new TextField();</span><br><span class="line">        Text stations = new Text(&quot;&quot;);</span><br><span class="line">        result.add(</span><br><span class="line">                new H3(&quot;换乘数: &quot;),</span><br><span class="line">                transferNum,</span><br><span class="line">                new H3(&quot;总距离 :&quot;),</span><br><span class="line">                distance,</span><br><span class="line">                new H3(&quot;途径站点详情:&quot;),</span><br><span class="line">                stations</span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        pathCalculate.addClickListener(click -&gt; &#123;</span><br><span class="line">            PathInfoVO pathResult = pathService.getPath(start.getValue(), end.getValue());</span><br><span class="line">            System.out.println(pathResult);</span><br><span class="line">            stations.setText(StringUtils.join(pathResult.getDetail(), &quot;,&quot;));</span><br><span class="line">            transferNum.setValue(String.valueOf(pathResult.getTransferNum()));</span><br><span class="line">            distance.setValue(String.valueOf(pathResult.getDistance()));</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        add(</span><br><span class="line">                new Text(&quot;hello world&quot;),</span><br><span class="line">                path,</span><br><span class="line">                pathCalculate,</span><br><span class="line">                result</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>In this code, we used Java code entirely to arrange the various components on the page. Even the button’s click handler is defined in a way familiar to Java developers, and it can directly call other backend services — making it nearly zero learning cost.</p><p>For detailed code and the running effect, you can check the <a href="https://github.com/lcy362/mobo-subway">project repository</a>.</p><p>If you’re interested in Vaadin or have any questions or thoughts, feel free to discuss in the comments section. Let’s explore how to better leverage Vaadin and improve our development efficiency together!</p><p>Source: <a href="https://lichuanyang.top/en/posts/43947/">https://lichuanyang.top/en/posts/43947/</a></p><hr><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><h3 id="Step-1-Environment-Setup"><a href="#Step-1-Environment-Setup" class="headerlink" title="Step 1: Environment Setup"></a>Step 1: Environment Setup</h3><p>Ensure JDK and Maven&#x2F;Gradle are installed. Spring Boot is recommended as the base framework. Install the Vaadin plugin in your IDE for a better development experience.</p><h3 id="Step-2-Create-Your-First-Vaadin-Project"><a href="#Step-2-Create-Your-First-Vaadin-Project" class="headerlink" title="Step 2: Create Your First Vaadin Project"></a>Step 2: Create Your First Vaadin Project</h3><p>Use Spring Initializr or the official Vaadin scaffolding to create a project, adding Vaadin dependencies. After starting the project, visit the default port to see the initial page.</p><h3 id="Step-3-Write-UI-Components"><a href="#Step-3-Write-UI-Components" class="headerlink" title="Step 3: Write UI Components"></a>Step 3: Write UI Components</h3><p>Use Vaadin’s component classes (such as <code>TextField</code>, <code>Button</code>, <code>VerticalLayout</code>, etc.) to build page layouts directly in Java code. Define page routes with the <code>@Route</code> annotation and set page titles with <code>@PageTitle</code>.</p><h3 id="Step-4-Data-Binding"><a href="#Step-4-Data-Binding" class="headerlink" title="Step 4: Data Binding"></a>Step 4: Data Binding</h3><p>Inject business services via <code>@Autowired</code>, call backend logic in button click events, and set the returned results to corresponding UI components to achieve frontend-backend data interaction.</p><h3 id="Step-5-Deployment"><a href="#Step-5-Deployment" class="headerlink" title="Step 5: Deployment"></a>Step 5: Deployment</h3><p>Package into an executable JAR file using <code>mvn package</code> or <code>gradle build</code>, then deploy to a server to run.</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/43947/</id>
    <link href="https://lichuanyang.top/en/posts/43947/"/>
    <published>2024-03-06T11:36:22.000Z</published>
    <summary>Introduction to how the Vaadin framework enables backend engineers to efficiently develop web interfaces without diving deep into the frontend ecosystem, solving the pain points of backend developers writing frontend code.</summary>
    <title>Vaadin Framework Tutorial: A Frontend Development Guide for Java Engineers</title>
    <updated>2026-06-27T03:50:04.987Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="hexo" scheme="https://lichuanyang.top/en/tags/hexo/"/>
    <category term="blog" scheme="https://lichuanyang.top/en/tags/blog/"/>
    <category term="i18n" scheme="https://lichuanyang.top/en/tags/i18n/"/>
    <content>
      <![CDATA[<h2 id="Common-Multi-language-Blog-Solutions"><a href="#Common-Multi-language-Blog-Solutions" class="headerlink" title="Common Multi-language Blog Solutions"></a>Common Multi-language Blog Solutions</h2><p>The simplest way to use hexo to build a multilingual website is to directly build two independent sites and make a jump link to each other. </p><p>Here’s how to implement it.</p><h2 id="Solution-Comparison"><a href="#Solution-Comparison" class="headerlink" title="Solution Comparison"></a>Solution Comparison</h2><p>Suppose I now have a Chinese blog and plan to add an English site</p><h2 id="Hexo-Dual-site-Implementation-Details"><a href="#Hexo-Dual-site-Implementation-Details" class="headerlink" title="Hexo Dual-site Implementation Details"></a>Hexo Dual-site Implementation Details</h2><ul><li><p>Copy the entire blog directory as the English blog directory. For example, my blog root directory is called blog.source, and copy a blog.source.en. After this, if source code of the blog is maintained using git, you can just create a new git project directly in the outer layer of the original directory and manage it in the outer layer. For example:</p>   <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">blog(git root directory)/blog.source</span><br><span class="line">                        /blog.source.en</span><br></pre></td></tr></table></figure></li><li><p>Delete all articles in the en directory; adjust text such as site description to English content; set the English language to en; set the English site root (root configuration in _config.yml) to &#x2F;en</p></li><li><p>Add jump links under two sites. I used the menu function to directly add an other language item to the menu. The configuration in the next theme is as follows:</p>   <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Chinese site:      English: /en || fa fa-language</span><br><span class="line">English site:      Chinese: https://lichuanyang.top || fa fa-language</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li><p>After that, the configuration is basically completed. You can write English articles in the en directory. The process is exactly the same as when writing Chinese articles before.</p></li></ul><h2 id="Deployment-and-CI-CD"><a href="#Deployment-and-CI-CD" class="headerlink" title="Deployment and CI&#x2F;CD"></a>Deployment and CI&#x2F;CD</h2><ul><li>The last step is to generate and publish. Pay attention to this step. Everytime we generate, we need to generate a Chinese site first, then generate an English site and copy the English content to the Chinese directory to avoid overwriting the English content when generating Chinese content.  Specific operation examples are as follows:<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean &amp;&amp; hexo g &amp;&amp;cd ../blog.source.en &amp;&amp; hexo clean &amp;&amp; hexo g &amp;&amp; cd ../blog.source &amp;&amp;cp -r ../blog.source.en/public/. public/en/ &amp;&amp; hexo s</span><br></pre></td></tr></table></figure>At the end of the command, use hexo s to start locally, use hexo d to publish it, just like normal use.</li></ul><p>In this way, we now have a multi-language site which is quite easy to use. After that, write Chinese content in the Chinese directory, write English content in the English directory, and finally execute the above command.</p><p>Original address： <a href="https://lichuanyang.top/en/posts/40400/">https://lichuanyang.top/en/posts/40400/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/40400/</id>
    <link href="https://lichuanyang.top/en/posts/40400/"/>
    <published>2024-01-23T10:41:29.000Z</published>
    <summary>
      <![CDATA[<h2 id="Common-Multi-language-Blog-Solutions"><a href="#Common-Multi-language-Blog-Solutions" class="headerlink" title="Common]]>
    </summary>
    <title>An Easy Way to Build a Multi-Language Blog with Hexo</title>
    <updated>2026-06-27T02:18:12.737Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="zhihu" scheme="https://lichuanyang.top/en/tags/zhihu/"/>
    <category term="tampermonkey" scheme="https://lichuanyang.top/en/tags/tampermonkey/"/>
    <category term="chrome-extension" scheme="https://lichuanyang.top/en/tags/chrome-extension/"/>
    <content>
      <![CDATA[<p>Zhihu, as a high-quality text community, often features deeply insightful comment discussions. However, Zhihu’s default comment timestamp format has a subtle but frustrating problem — it only shows relative times like “3 hours ago,” “yesterday,” or “5 days ago.” That’s fine for casual browsing, but if you’re trying to trace the timeline of a discussion or compare the order of different comments, this fuzzy time display is completely inadequate.</p><span id="more"></span><h2 id="The-Problem-Limitations-of-Relative-Time"><a href="#The-Problem-Limitations-of-Relative-Time" class="headerlink" title="The Problem: Limitations of Relative Time"></a>The Problem: Limitations of Relative Time</h2><p>Relative timestamps come with a few clear downsides:</p><ol><li><strong>Information loss</strong>: You never know exactly what hour and minute “3 hours ago” actually refers to — there’s no way to form a precise mental timestamp anchor</li><li><strong>Cross-day confusion</strong>: After today passes, “yesterday” becomes “the day before yesterday,” and the reference frame keeps shifting</li><li><strong>Discussion tracking is hard</strong>: In a technical discussion, person A might reply to person B’s point, but B’s comment and A’s reply might both show “2 days ago” — you can’t tell who posted first</li></ol><h2 id="How-This-Script-Came-About"><a href="#How-This-Script-Came-About" class="headerlink" title="How This Script Came About"></a>How This Script Came About</h2><p>I previously saw a complaint on <a href="https://meta.appinn.net/t/topic/47711/18">Appinn</a> about the chaotic time display in Zhihu’s comment sections. Someone in that thread quickly whipped up a Tampermonkey script to solve the problem right then and there.</p><p>I used it for several months and the experience was excellent. Then, a while back, Zhihu pushed a frontend update that broke the script — time display became messy again. I spent an afternoon digging into Zhihu’s new DOM structure, fixed the script’s logic, and decided to publish it on GreasyFork. This way, if Zhihu makes more adjustments in the future, updates can be pushed directly from GreasyFork — no need for manual reinstallation.</p><h2 id="Installation-Usage"><a href="#Installation-Usage" class="headerlink" title="Installation &amp; Usage"></a>Installation &amp; Usage</h2><p>The script is published on GreasyFork:</p><p><strong><a href="https://greasyfork.org/zh-CN/scripts/482871-%E7%9F%A5%E4%B9%8E%E5%A2%9E%E5%BC%BA-%E8%AF%84%E8%AE%BA%E6%97%B6%E9%97%B4%E7%B2%BE%E7%A1%AE%E5%88%B0%E7%A7%92">Install link</a></strong></p><p>Prerequisite: you need the Tampermonkey extension installed in your browser:</p><ul><li><strong>Chrome</strong>: <a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo">Chrome Web Store</a></li><li><strong>Edge</strong>: Search for Tampermonkey directly in the Edge Add-ons store</li><li><strong>Firefox</strong>: <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Firefox Add-ons</a></li></ul><p>Once Tampermonkey is installed, click the GreasyFork link above and then click “Install this script.” The script will automatically take effect in Zhihu’s comment sections — no additional configuration needed.</p><h2 id="Technical-Implementation-Overview"><a href="#Technical-Implementation-Overview" class="headerlink" title="Technical Implementation Overview"></a>Technical Implementation Overview</h2><p>The core logic of the script isn’t complicated. It comes down to three key steps:</p><p><strong>1. Locating the Time Elements</strong></p><p>Zhihu stores comment timestamp information on a <code>span</code> element. In the old version, the full timestamp was stored in the <code>title</code> attribute. After Zhihu’s update, they switched to a different attribute, so the script needed to adapt to the new DOM structure.</p><p><strong>2. Extraction &amp; Formatting</strong></p><p>The script reads the raw ISO time string from the DOM element (e.g., <code>2024-01-15T14:32:08.000Z</code>) and formats it into a human-readable format like <code>2024-01-15 14:32:08</code> using JavaScript, replacing the original relative time text.</p><p><strong>3. Handling Dynamic Loading</strong></p><p>Zhihu’s comment sections use lazy loading — more comments only load as you scroll to the bottom. The script uses a <code>MutationObserver</code> to listen for DOM changes, so when new comments are inserted into the page, it automatically applies the same precise timestamp treatment to them.</p><h2 id="Future-Plans"><a href="#Future-Plans" class="headerlink" title="Future Plans"></a>Future Plans</h2><p>I’m planning to extend this script with more features, such as:</p><ul><li><strong>One-click jump to referenced comments</strong>: Zhihu’s reply format is “replying to xxx,” but there’s no way to jump directly to the quoted comment</li><li><strong>Highlight OP (Original Poster) comments</strong>: Quickly spot the question author’s replies in long discussions</li><li><strong>Comment export</strong>: Export insightful discussions as Markdown</li></ul><p>If you have other pain points with the Zhihu experience, feel free to leave a comment with your feature requests. If there’s interest in Tampermonkey script development, I might consider putting together a beginner’s tutorial on the topic in the future.</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/44912/</id>
    <link href="https://lichuanyang.top/en/posts/44912/"/>
    <published>2023-12-22T09:24:18.000Z</published>
    <summary>Tampermonkey script: Display Zhihu comment timestamps with second-level precision, fixing the time display issues after Zhihu's update.</summary>
    <title>Zhihu Enhancement Tool — Comment Timestamps Down to the Second</title>
    <updated>2026-06-27T00:37:08.383Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="zhihu" scheme="https://lichuanyang.top/en/tags/zhihu/"/>
    <category term="tampermonkey" scheme="https://lichuanyang.top/en/tags/tampermonkey/"/>
    <category term="chrome-extension" scheme="https://lichuanyang.top/en/tags/chrome-extension/"/>
    <content>
      <![CDATA[<p>Everyone knows what kind of website Zhihu is. Today, let’s look at it from a different angle — I’m sharing a small tool to make “browsing Zhihu at work” safer and more efficient.</p><span id="more"></span><h2 id="The-Economics-of-Browsing-at-Work-Why-Zhihu"><a href="#The-Economics-of-Browsing-at-Work-Why-Zhihu" class="headerlink" title="The Economics of Browsing at Work: Why Zhihu?"></a>The Economics of Browsing at Work: Why Zhihu?</h2><p>I was looking through my own Zhihu account analytics recently and noticed something quite interesting: <strong>over 40% of traffic came from the PC side</strong>. In the mobile internet era, that number is unusually high.</p><p>I also learned from other sources that Zhihu’s traffic on weekends and holidays is noticeably lower than on workdays. What does this tell us?</p><p><strong>You’re not the only one browsing Zhihu at work.</strong></p><p>For a significant portion of people, Zhihu’s primary use case is — browsing during work hours. And Zhihu happens to be uniquely suited for this:</p><ul><li><strong>Primarily text-based content</strong>: Unlike short videos, there’s no sound and minimal data usage</li><li><strong>Plenty of professional content</strong>: When your boss walks by and glances at your screen, you can genuinely say “I’m looking up technical material”</li><li><strong>Right-sized articles</strong>: A timeline of posts you can read a few minutes at a time, with very little mental overhead</li></ul><h2 id="Three-Pain-Points-of-Browsing-Zhihu-at-Work"><a href="#Three-Pain-Points-of-Browsing-Zhihu-at-Work" class="headerlink" title="Three Pain Points of Browsing Zhihu at Work"></a>Three Pain Points of Browsing Zhihu at Work</h2><p>However, some of Zhihu’s PC design choices aren’t exactly friendly for the covert browser:</p><p><strong>1. Oversized Images</strong></p><p>Article images on Zhihu often take up more than half the screen. No matter how serious the article itself is, a giant image on your display is instantly noticeable to anyone passing by — at that point, saying “I was reading a technical article” doesn’t sound very convincing.</p><p><strong>2. Attention-Grabbing Title Banner</strong></p><p>When browsing Zhihu questions, there’s a very conspicuous title banner hovering at the top of the page. It doesn’t just take up a full line of space — its white background with black text makes it extremely eye-catching. A passerby can tell at a glance you’re on Zhihu.</p><p><strong>3. In-Your-Face Logo</strong></p><p>Zhihu’s logo sits prominently in the top-left corner of every page. Anytime someone glances at your screen or you take a screenshot, that brand identity is unmistakable.</p><h2 id="The-Script-Solution"><a href="#The-Script-Solution" class="headerlink" title="The Script Solution"></a>The Script Solution</h2><p>I wrote a Tampermonkey script that targets these three problems head-on:</p><ul><li><strong>Limit image size</strong>: Maximum width set to 300px, turning all article images into unobtrusive thumbnails</li><li><strong>Hide the title banner</strong>: The question page title banner is removed entirely</li><li><strong>Remove the Zhihu logo</strong>: The logo in the top-left corner is hidden</li></ul><p>The result: Zhihu pages end up looking like a plain text reader — clean, low-key, and perfectly suited for an office environment.</p><h2 id="Installation-Usage"><a href="#Installation-Usage" class="headerlink" title="Installation &amp; Usage"></a>Installation &amp; Usage</h2><p>The script is published on GreasyFork:</p><p><strong><a href="https://greasyfork.org/zh-CN/scripts/483047-%E6%91%B8%E9%B1%BC%E5%8A%A9%E6%89%8B-%E7%9F%A5%E4%B9%8E">Install link</a></strong></p><p>Prerequisite: you need the Tampermonkey extension installed in your browser. Once installed, the script takes effect automatically — no configuration required.</p><h2 id="Technical-Implementation"><a href="#Technical-Implementation" class="headerlink" title="Technical Implementation"></a>Technical Implementation</h2><p>The script’s implementation is actually extremely simple. At its core, it just injects custom CSS via <code>GM_addStyle</code>:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Limit maximum image width</span></span><br><span class="line"><span class="title function_">GM_addStyle</span>(<span class="string">&#x27;.RichContent img &#123; max-width: 300px !important; &#125;&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Hide the title banner</span></span><br><span class="line"><span class="title function_">GM_addStyle</span>(<span class="string">&#x27;.QuestionHeader &#123; display: none !important; &#125;&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Remove the logo</span></span><br><span class="line"><span class="title function_">GM_addStyle</span>(<span class="string">&#x27;.AppHeader-logo &#123; display: none !important; &#125;&#x27;</span>);</span><br></pre></td></tr></table></figure><p>Essentially just a few lines of CSS — but remarkably effective. That’s part of the beauty of Tampermonkey scripts: often you don’t need complex logic; just injecting some CSS can dramatically change a webpage’s visual experience.</p><h2 id="How-It-Differs-from-the-“Zhihu-Enhancement-Tool”"><a href="#How-It-Differs-from-the-“Zhihu-Enhancement-Tool”" class="headerlink" title="How It Differs from the “Zhihu Enhancement Tool”"></a>How It Differs from the “Zhihu Enhancement Tool”</h2><p>I previously wrote a <a href="/posts/44912/">Zhihu Enhancement Tool</a>, which focuses on <strong>information enhancement</strong> — displaying comment timestamps down to the second. This script, by contrast, is about <strong>visual optimization</strong> — making Zhihu look more like a legitimate reading tool in a workplace setting.</p><p>Both scripts can be installed simultaneously; they don’t conflict with each other.</p><p>If you have other pain points with the Zhihu experience, feel free to leave a comment. You might also want to check out my other <a href="/posts/44912/">Zhihu Enhancement Tool</a> — it shows comment timestamps down to the second, which is great for tracing the timeline of technical discussions.</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/23393/</id>
    <link href="https://lichuanyang.top/en/posts/23393/"/>
    <published>2022-12-28T10:29:01.000Z</published>
    <summary>Sharing a Tampermonkey script that optimizes the Zhihu PC experience for more efficient browsing during work hours.</summary>
    <title>How to Scientifically Browse Zhihu at Work</title>
    <updated>2026-06-27T00:37:22.426Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Miscellany" scheme="https://lichuanyang.top/en/categories/Tech-Miscellany/"/>
    <category term="personal-growth" scheme="https://lichuanyang.top/en/tags/personal-growth/"/>
    <category term="book-reflections" scheme="https://lichuanyang.top/en/tags/book-reflections/"/>
    <category term="systems-thinking" scheme="https://lichuanyang.top/en/tags/systems-thinking/"/>
    <category term="logical-thinking" scheme="https://lichuanyang.top/en/tags/logical-thinking/"/>
    <content>
      <![CDATA[<p>The primary purpose of this book is to teach readers how to think more effectively when facing complex problems in reality. The book itself focuses on basic systems concepts, which are actually quite easy to understand. Additionally, the author provides numerous examples. When first reading this book, you might feel that the theory and practical examples are somewhat disconnected. But gradually you’ll realize that this topic is indeed not easy to explain in a way that’s easily accessible — it requires readers to put in more thought to gain substantive understanding. The examples provided by the author serve as excellent prompts for guiding readers to think. By following the author’s thinking through these examples, we can better grasp the concepts and logic presented in the book.</p><span id="more"></span><p>So, in these notes, I mainly extract some of the book’s core concepts in a relatively mechanical way, while also attempting to provide some everyday examples. For a better understanding of each section, I recommend reading the book itself and following the author’s reasoning to think through the logic of those examples.</p><h2 id="Basic-Concepts"><a href="#Basic-Concepts" class="headerlink" title="Basic Concepts"></a>Basic Concepts</h2><p>A system is a set of interconnected things that interact with each other over time in specific behavioral patterns.</p><p>Any system consists of three components: elements, connections, and function or purpose.</p><p>Stocks are the foundation of all systems — such as water in a bathtub, population size, books in a bookstore, the volume of trees, money in the bank, and so on. However, stocks don’t necessarily have to be physical; your confidence, your good reputation among friends, or your hopes for the world can all be stocks.</p><p>Stocks change over time, and what causes these changes are “flows.”</p><p>Drawing stock-flow diagrams is a fundamental way to understand systems.</p><p>When a change in one stock affects the inflow or outflow associated with it, a feedback loop is formed.</p><p>Balancing loops and reinforcing loops are two common types of feedback loops — they’re quite intuitive and mean exactly what they sound like. Balancing loops tend to keep system stocks stable, while reinforcing loops continuously amplify and strengthen existing trends.</p><h2 id="Common-Systems"><a href="#Common-Systems" class="headerlink" title="Common Systems"></a>Common Systems</h2><p>Next, let’s explore some common system models to help better understand what a system is.</p><h2 id="Single-Stock-Systems"><a href="#Single-Stock-Systems" class="headerlink" title="Single-Stock Systems"></a>Single-Stock Systems</h2><ul><li><p>System 1.1: A system with one stock and two opposing balancing loops, such as a thermostat, which uses a heating balancing loop and a cooling balancing loop to keep the stock (temperature) stable.</p></li><li><p>System 1.2: A system with one stock, one reinforcing loop, and one balancing loop, such as population — births form the reinforcing loop, and deaths form the balancing loop.</p></li><li><p>System 1.3: A system with time delays, such as inventory. Compared to a simple thermostat, the main difference is that there are delays in the balancing loops.</p></li></ul><h2 id="Double-Stock-Systems"><a href="#Double-Stock-Systems" class="headerlink" title="Double-Stock Systems"></a>Double-Stock Systems</h2><ul><li><p>System 2.1: A system where a renewable stock is constrained by a non-renewable stock, such as extracting oil or other non-renewable resources. In this case, there are two stocks — capital and resources. There is a reinforcing loop: extracting resources generates profits, which increase capital, expand production capacity, and increase extraction. Meanwhile, since oil is non-renewable, extraction becomes increasingly difficult, forming a balancing loop.</p></li><li><p>System 2.2: A system with two renewable stocks, such as fisheries. It’s similar to the oil system, except fish are renewable resources, which leads to different developmental patterns compared to the previous system type.</p></li></ul><h2 id="Three-Key-Characteristics-of-Systems"><a href="#Three-Key-Characteristics-of-Systems" class="headerlink" title="Three Key Characteristics of Systems"></a>Three Key Characteristics of Systems</h2><ul><li><p>Resilience. Systems have resilience because their internal structures contain many interacting feedback loops. These loops support each other, allowing the system to recover to its original state through various means even when subjected to major disturbances. The human body, for example, is a highly resilient system.</p></li><li><p>Self-organization. The ability of a system to make its own structure more complex is called “self-organization.” For systems, self-organization means the system can “evolve” on its own to accomplish larger goals or implement more complex functions. My former boss often told me our goal was to build a team with “self-organization” capabilities. Only then can the team progress alongside business development.</p></li><li><p>Hierarchy. As new structures emerge and complexity increases, self-organizing systems often generate hierarchies or levels. A large system contains many subsystems, which can be decomposed into even more, smaller subsystems. If each subsystem can basically maintain itself, perform certain functions, and serve the needs of a larger system — while the larger system is responsible for regulating and strengthening the operations of each subsystem — then relatively stable, resilient, and efficient structures can emerge and be maintained.</p></li></ul><h2 id="System-Pitfalls"><a href="#System-Pitfalls" class="headerlink" title="System Pitfalls"></a>System Pitfalls</h2><p>Everything we think we know about the world is just a model. Our models are usually highly consistent with reality. This is why we’ve become one of the most successful species on the planet. But our models are still far from being able to fully depict the world. This is why we often make mistakes and are frequently surprised.</p><p>So, let’s examine what problems can be identified from a systems perspective, to help us make fewer mistakes. The author mainly organized the following six points:</p><ul><li>System structure is the root cause of behavior, while system behavior manifests as a series of events over time; yet people tend to be misled by surface appearances</li><li>In a nonlinear world, don’t use linear thinking patterns</li><li>Need to appropriately define system boundaries</li><li>Must be able to see various limiting factors</li><li>Time delays are ubiquitous in systems</li><li>People have only bounded rationality, leading to decisions that are often not globally optimal</li></ul><h2 id="System-Traps-and-Countermeasures"><a href="#System-Traps-and-Countermeasures" class="headerlink" title="System Traps and Countermeasures"></a>System Traps and Countermeasures</h2><p>This chapter covers more specific problems and potential countermeasures for addressing them.</p><h2 id="Policy-Resistance-Treating-Symptoms-Rather-Than-Root-Causes"><a href="#Policy-Resistance-Treating-Symptoms-Rather-Than-Root-Causes" class="headerlink" title="Policy Resistance; Treating Symptoms Rather Than Root Causes"></a>Policy Resistance; Treating Symptoms Rather Than Root Causes</h2><p>Some long-term behavioral patterns may not match people’s expectations and are often seen as problems. Despite people developing various technologies and implementing policy measures to “fix” them, the system seems stubborn, producing the same behavior year after year. This is a common system trap, commonly referred to as “treating symptoms rather than root causes” or “policy resistance,” such as drug proliferation and unemployment.</p><p>“Policy resistance” comes from the bounded rationality of various participants in the system, each with their own goals. When the goals of different subsystems are different or inconsistent, resistance to change emerges. The most effective way to address “policy resistance” is to try to align the goals of various subsystems — typically by establishing a larger overarching goal that allows all participants to transcend their individual bounded rationality.</p><h2 id="Tragedy-of-the-Commons"><a href="#Tragedy-of-the-Commons" class="headerlink" title="Tragedy of the Commons"></a>Tragedy of the Commons</h2><p>For resources shared by people, it’s easy for exploitation (or consumption) to gradually escalate or grow. An important reason the “Tragedy of the Commons” arises is that the feedback between resource consumption and the growth of resource users is missing, or the time delay is too long.</p><p>There are three ways to prevent the “Tragedy of the Commons”: first, education to help people see consequences more clearly; second, privatization of resources; third, implementing regulatory measures such as quota systems.</p><h2 id="Drift-to-Low-Performance"><a href="#Drift-to-Low-Performance" class="headerlink" title="Drift to Low Performance"></a>Drift to Low Performance</h2><p>Performance standards are influenced by past performance, especially when people evaluate past performance too negatively — that is, focusing too much on bad news. This triggers a vicious cycle that continuously lowers both goals and system performance levels. In plain terms, it’s like boiling a frog — the situation deteriorates gradually without being noticed.</p><p>The countermeasure is to maintain an absolute performance standard. A better approach is to set the performance standard at the past best level, thereby continuously raising one’s own goals and using this to motivate improvement.</p><h2 id="Escalation"><a href="#Escalation" class="headerlink" title="Escalation"></a>Escalation</h2><p>“An eye for an eye, a tooth for a tooth.” Each participant’s desired system state is relative to other participants, and they try to surpass them, staying one step ahead — even being tied is unacceptable. Moreover, each participant tends to overestimate the other’s hostility and exaggerate the other’s strength. The system structure of escalation is a reinforcing loop that develops exponentially — once it exceeds a certain threshold, the speed at which competition intensifies will surpass most people’s imagination.</p><p>One countermeasure relies on one party making a concession; a more elegant approach is for both parties to reach an agreement.</p><h2 id="Success-to-the-Successful"><a href="#Success-to-the-Successful" class="headerlink" title="Success to the Successful"></a>Success to the Successful</h2><p>Using accumulated wealth, power, special channels, or insider information can create even more wealth, power, channels, and information. These are all examples of another archetype called “Success to the Successful.”</p><p>Countermeasures: diversification — allowing the losing party in competition to exit and start a new game; antitrust laws — strictly limiting the maximum share the winner can hold; modifying competition rules — limiting the advantages of the strongest participants or giving special consideration to disadvantaged participants to enhance their competitiveness (such as charity, gifts, tax adjustments, transfer payments, etc.); giving diverse rewards to winners to prevent them from competing for the same limited resources or developing biases in the next round of competition.</p><h2 id="Shifting-the-Burden"><a href="#Shifting-the-Burden" class="headerlink" title="Shifting the Burden"></a>Shifting the Burden</h2><p>When facing a systemic problem, if the solution adopted doesn’t address the underlying root cause at all but merely alleviates (or masks) the symptoms, it will lead to shifting the burden, dependency, and addiction.</p><p>The best way to deal with this trap is prevention — don’t fall into the trap in the first place. Always be aware that policies or practices that merely alleviate symptoms or mask signals cannot truly solve problems.</p><h2 id="Rule-Beating"><a href="#Rule-Beating" class="headerlink" title="Rule Beating"></a>Rule Beating</h2><p>Any rules may have loopholes or exceptions, creating opportunities to beat the rules. This means that although some behaviors may appear to comply with or not violate the rules on the surface, they don’t actually align with the spirit of the rules, or even distort the system.</p><p>The countermeasure is to design or redesign rules, gaining creative feedback from rule-beating behaviors so that they serve a positive function and achieve the original purpose of the rules.</p><h2 id="Goal-Misalignment"><a href="#Goal-Misalignment" class="headerlink" title="Goal Misalignment"></a>Goal Misalignment</h2><p>System behavior is particularly sensitive to the goals of feedback loops. If goals are inaccurately or incompletely defined, even if the system faithfully executes all operational rules, the output may not be what people actually want.</p><p>The countermeasure is to appropriately set goals and indicators that reflect the true welfare of the system.</p><h2 id="Ways-to-Change-Systems-Leverage-Points-for-Intervening-in-Systems"><a href="#Ways-to-Change-Systems-Leverage-Points-for-Intervening-in-Systems" class="headerlink" title="Ways to Change Systems: Leverage Points for Intervening in Systems"></a>Ways to Change Systems: Leverage Points for Intervening in Systems</h2><p>This section covers ways we can intervene in systems, ranked from least to most effective. Many of the theories in this chapter have probably been encountered elsewhere to some extent. Many excellent people, even without reading this book, actually think this way in many situations. The book “Thinking in Systems” helps us understand more systematically why we should do it this way.</p><p>12: Numbers — regulating the system through the numerical values of various flows. Like the balance designers in Honor of Kings, most of their energy goes into this: when a hero’s mechanics are too strong, they heavily nerf the numbers; when a hero isn’t performing well, they add numbers to make them viable even in simple face-to-face combat. This approach is actually quite low in effectiveness — it cannot change the fundamental structure of the system. But most people focus 90% of their attention on parameters.</p><p>11: Buffers — By increasing buffer capacity, we can usually stabilize the system. However, if buffers are too large, the system will become inflexible, and its response speed to changes will be too slow. At the same time, establishing, expanding, or maintaining buffer capacity requires significant time and capital, such as building reservoirs or warehouses. For this reason, some enterprises have invented “zero inventory” and “just-in-time” production models. In their view, compared to spending huge sums to maintain fixed inventory, the losses from occasional fluctuations or stockouts are not that significant.</p><ol start="10"><li><p>Stock-flow structure: Physical systems and their intersection nodes. This mainly concerns the overall design of the system. Appropriate leverage points need to be designed correctly from the beginning. Once the physical structure is established, finding leverage points requires understanding the system’s limits and bottlenecks, maximizing their efficiency while avoiding major fluctuations or expansions that exceed their capacity. If the system is already running, adjusting key nodes becomes very difficult. The well-known “anti-corruption layer” concept in software engineering can optimize this problem to some extent by using pre-set intermediate layers to reduce the cost of adjusting key nodes.</p></li><li><p>Time delays: The speed at which the system responds to changes. Time delays are a high-leverage point, but in practice, time delays are usually not easy to change. The development of many things follows its own internal laws — it takes however long it takes. You can’t accumulate a large amount of capital overnight, children can’t grow up overnight, and pulling seedlings to help them grow faster won’t speed up crop growth. But if there’s a way to change time delays, it can often produce significant results. For example, in recent pandemic prevention and control efforts, the reason for frequently conducting large-scale nucleic acid testing is to reduce the time delay between virus transmission and detection.</p></li><li><p>Balancing loops: The feedback force that attempts to correct external influences. This is easy to understand — we can strengthen control over the system by adding balancing loops. The controllers in Kubernetes can be considered implementations of balancing loops, which use different controllers to control the values (stocks) of different components at their desired levels.</p></li><li><p>Reinforcing loops: The feedback force that drives the growth of returns. Similar to balancing loops, but with a different goal.</p></li><li><p>Information flows: The structure of who can access information. This is equivalent to a new loop that allows people to get feedback in places where they previously couldn’t. For example, the capitalistic practice of ranking overtime hours tells you about colleagues’ overtime information, naturally creating pressure that promotes overtime work.</p></li><li><p>System rules: Incentives, penalties, and constraints. This section essentially defines the scope, boundaries, and degrees of freedom of the system.</p></li><li><p>Self-organization: The force that increases, changes, or evolves system structure. Returning to the topic at the beginning, building a self-organizing team requires doing many things, such as improving each team member’s capabilities so everyone has decision-making ability, and enhancing information transparency within the team. Once such a team is built, not only will output improve qualitatively, but team members’ work satisfaction and happiness will also increase significantly.</p></li><li><p>Goals: The purpose or function of the system. A participant in the system can clearly set, articulate, repeat, support, and persist in new goals, thereby guiding the system’s transformation. This is why OKRs have become so popular in recent years. Through clear and reasonable OKRs, people are guided to make better decisions and take better actions.</p></li><li><p>Social paradigms: The mental models that determine what a system is. Some socially recognized concepts, some underlying basic assumptions, and widespread views about the nature of social reality constitute the social paradigm — or a whole worldview. These are a series of basic assumptions, rules, or beliefs that people generally hold about how the world works. These beliefs are implicit because in a society, almost everyone already knows them and thus there’s no need to specifically declare them. These paradigms naturally have a huge impact on system operations, and of course, changing them is more difficult than changing other things.</p></li><li><p>Transcending paradigms. Compared to changing paradigms, at a higher level, there is another leverage point: freeing oneself from the control of any paradigm. This point has a somewhat metaphysical quality, so we won’t elaborate much here. The core idea is that we need to consciously step outside the current system.</p></li></ol><h2 id="Rules-for-Living-with-Systems-Dancing-with-Systems"><a href="#Rules-for-Living-with-Systems-Dancing-with-Systems" class="headerlink" title="Rules for Living with Systems: Dancing with Systems"></a>Rules for Living with Systems: Dancing with Systems</h2><p>People who grew up in industrial society and are enthusiastic about systems thinking are likely to make a serious mistake. They may assume that through systems analysis, they can understand the interconnections and complex entanglements within systems, and with the power of computers, ultimately find the key to predicting and controlling systems. Unfortunately, this is a mistaken notion rooted in the deep-seated mental model of the industrial era — the belief that there exists a key to prediction and control. But in reality, achieving this is completely unrealistic. We need to recognize and be willing to give up the illusion of control, and adopt a completely different approach. This way, we can still make significant contributions. This approach is dancing with systems. We cannot control systems, but we can live better within them.</p><p>Next are some rules for dancing with systems — this section is essentially a collection of tips, all fairly easy to understand, so we’ll list them briefly:</p><ul><li>Keep up with the rhythm of the system: You need to observe how the system operates in order to keep up with it. We need to pay more attention to facts and data so as not to be led astray by other people’s theories.</li><li>Expose your mental models to the light, draw system structure diagrams, force yourself to project various hidden assumptions from within, and express them precisely.</li><li>Trust, respect, and share information</li><li>Use language carefully and enrich it with systems concepts</li><li>Focus on what’s important, not just what’s easy to measure</li><li>Design policies with feedback functions for feedback systems. For dynamic, self-regulating feedback systems, static, rigid policies cannot be used for regulation. Good policies must be able to flexibly adjust in a timely manner according to changes in system state.</li><li>Pursue overall interests</li><li>Listen to the wisdom of the system</li><li>Define system responsibilities</li><li>Stay humble, be a learner</li><li>Celebrate complexity</li><li>Expand the time horizon</li><li>Break through various taboos and rigid rules</li><li>Expand the scope of concern</li><li>Don’t lower the standards of “good”</li></ul><p>Source: <a href="https://lichuanyang.top/en/posts/53791/">https://lichuanyang.top/en/posts/53791/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/53791/</id>
    <link href="https://lichuanyang.top/en/posts/53791/"/>
    <published>2022-05-16T10:34:01.000Z</published>
    <summary>Book notes on 'Thinking in Systems', learning systematic thinking methods for facing complex problems and building the right mental frameworks.</summary>
    <title>Book Notes: Thinking in Systems - How to Face Complex Problems in Reality</title>
    <updated>2026-06-27T02:18:32.206Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="distributed-systems" scheme="https://lichuanyang.top/en/tags/distributed-systems/"/>
    <category term="distributed-system-design" scheme="https://lichuanyang.top/en/tags/distributed-system-design/"/>
    <content>
      <![CDATA[<p>Previously, I translated an article about distributed systems (<a href="https://lichuanyang.top/posts/3914/">https://lichuanyang.top/posts/3914/</a>) which received positive feedback across various platforms. So I recently reorganized the related knowledge, combined with new insights gained over the past year, and rewrote this article.</p><span id="more"></span><p>First, let’s clarify what kind of distributed systems we’re discussing here. Simply put, they need to satisfy two conditions: <strong>multi-node</strong> and <strong>stateful</strong>. Multi-node is straightforward to understand. Being stateful means the system needs to maintain some data — otherwise, we could simply scale horizontally without limit, and there would be no distributed systems problem to speak of.</p><p>Common distributed systems — whether databases like MySQL, Cassandra, HBase, message queues like RocketMQ, Kafka, Pulsar, or infrastructure like ZooKeeper — all satisfy these two conditions.</p><p>The implementation of these distributed systems typically needs to address two aspects: first, the system’s own functionality; and second, maintaining good performance and stability in a distributed environment. Even two systems with completely different functions will have many similarities in how they address the second type of problem. This article focuses on the second type.</p><h2 id="Core-Challenges-of-Distributed-Systems"><a href="#Core-Challenges-of-Distributed-Systems" class="headerlink" title="Core Challenges of Distributed Systems"></a>Core Challenges of Distributed Systems</h2><p>Next, let’s enumerate the common goals of distributed systems, including but not limited to:</p><ul><li>A large number of ordinary servers interconnected via a network, collectively providing services to the outside world.</li><li>As the cluster scales up, overall system performance scales linearly.</li><li>Automatic fault tolerance — failed nodes are automatically migrated, and data consistency across different nodes is maintained.</li></ul><p>What are the challenges in achieving these goals? Here are the main ones:</p><ol><li><p><strong>Process crashes</strong>: Caused by many factors, including hardware failures, software bugs, routine maintenance, and in cloud environments, more complex reasons. The biggest problem with process crashes is data loss. For performance reasons, we often don’t write synchronously to disk — instead, we temporarily buffer data in memory and periodically flush to disk. When a process crashes, the data in memory buffers is clearly lost.</p></li><li><p><strong>Network delays and interruptions</strong>: When communication between nodes becomes very slow, how does one node determine whether another node is healthy?</p></li><li><p><strong>Network partitions</strong>: The cluster’s nodes split into two subsets — communication within each subset works, but communication between subsets is broken (brain split). How should the cluster continue to serve requests?</p><p>A brief digression: under the CAP theorem, real-world systems typically adopt one of two modes — CP mode (giving up high availability) or AP mode (giving up strong consistency). Why is there no CA mode that gives up partition tolerance? Because we cannot assume network communication will always be reliable. Once a cluster splits into two partitions, re-merging is not realistic.</p></li><li><p><strong>Process pauses</strong>: For example, a full GC may cause a process to become temporarily unavailable before quickly recovering. During the unavailable period, the cluster may have already reacted accordingly. How should state consistency be maintained when the node comes back?</p></li><li><p><strong>Clock skew and message reordering</strong>: We want operations across different nodes in the cluster to have a clear ordering. Clock skew between nodes prevents us from using timestamps to guarantee this. Message reordering further complicates distributed system processing.</p></li></ol><p>Now, let’s walk through the solutions to these problems.</p><h2 id="Partitioning-and-Replication"><a href="#Partitioning-and-Replication" class="headerlink" title="Partitioning and Replication"></a>Partitioning and Replication</h2><p>For process crashes, the key point is that simply ensuring no data loss during a crash is not difficult at all — the real challenge is achieving this while maintaining system performance.</p><p>The first pattern to introduce is <strong>Write-Ahead Log (WAL)</strong>: the server stores every state change as a command in an append-only file on disk. Since append operations are sequential disk writes, they are typically very fast and can be completed without impacting performance. During server failure recovery, the log can be replayed to rebuild in-memory state.</p><p>The core idea is to first persist data in a low-cost way (not necessarily limited to sequential disk writes), then acknowledge the client without blocking other operations. The server asynchronously performs the subsequent high-cost work.</p><p>Typical scenarios and variants: MySQL redo log; Redis AOF; Kafka itself; common practices in application development — for time-consuming operations, first write a database record indicating the task will be executed, then perform the actual task asynchronously.</p><p>WAL introduces a minor issue: the log keeps growing. How to handle its own storage? Two natural approaches: <strong>segmentation</strong> and <strong>cleanup</strong>.</p><p>Segmentation splits a large log into smaller ones. Since WAL logic is generally simple, splitting is not complex — much easier than typical database sharding. This pattern is called <strong>Segmented Log</strong>, and the canonical implementation is Kafka’s partitions.</p><p>For cleanup, there is a pattern called <strong>low-water mark</strong>: a marker indicating which parts of the log can be cleaned up. The marking can be based on data status (redo log), preset retention time (Kafka), or more fine-grained cleanup and compaction (AOF).</p><h2 id="Consistency-Protocols"><a href="#Consistency-Protocols" class="headerlink" title="Consistency Protocols"></a>Consistency Protocols</h2><p>Moving to network-related issues, a simple <strong>heartbeat</strong> pattern solves inter-node state synchronization. If no heartbeat is received within a time window, the node is considered down.</p><p>For brain-split scenarios, the <strong>Quorum</strong> pattern is typically used. It requires the number of live nodes in the cluster to reach a Quorum value (typically, with 2f+1 nodes, at most f nodes can go down, so the Quorum value is f+1) before the cluster can serve requests. When examining implementations of distributed systems like RocketMQ or ZooKeeper, you’ll find the requirement for a minimum number of live nodes — this is exactly the Quorum pattern.</p><p>Quorum solves the data durability problem — successfully written data will not be lost even if nodes fail. However, Quorum alone cannot guarantee strong consistency, because data on different nodes may differ in time. When clients connect to different nodes, they may see different results. This can be solved through the <strong>Leader and Followers</strong> pattern, where one node is elected as the leader to coordinate data replication across nodes and determine which data is visible to clients.</p><p>The <strong>High-Water Mark</strong> pattern determines which data is visible to clients. Generally, after a write is completed on a Quorum number of follower nodes, the data can be marked as visible to clients. The line of completed replication is the high-water mark.</p><p>The Leader-Followers pattern is so widely used that examples are unnecessary. There are many distributed election algorithms — Bully, ZAB, Paxos, Raft, among others. Paxos is difficult both to understand and implement. Bully triggers frequent elections when nodes go up and down frequently. Raft, on the other hand, strikes a relatively balanced trade-off in terms of stability, implementation complexity, and adoption — it is the most widely used distributed election algorithm. For example, Elasticsearch replaced Bully with Raft in version 7.0; Kafka 2.8 switched from ZooKeeper’s ZAB protocol to Raft.</p><p>At this point, let’s summarize. Essentially, an operation on a distributed system can be概括为 the following steps:</p><ol><li>Write to the leader’s Write-Ahead Log</li><li>Write to 1 follower’s WAL</li><li>Write to the leader’s data store</li><li>Write to 1 follower’s data store</li><li>Write to a Quorum of followers’ WALs</li><li>Write to a Quorum of followers’ data stores</li></ol><p>The order of steps 2–5 is not fixed. The most important way a distributed system balances performance and stability is, in essence, deciding the order of these operations and when to return a success acknowledgment to the client. For example, MySQL’s synchronous replication, asynchronous replication, and semi-synchronous replication are classic examples of these different orderings.</p><h2 id="Fault-Detection-and-Recovery"><a href="#Fault-Detection-and-Recovery" class="headerlink" title="Fault Detection and Recovery"></a>Fault Detection and Recovery</h2><p>For process pauses, the main problem scenario is: suppose the leader pauses. During the pause, a new leader is elected. When the original leader recovers, what should happen? The <strong>Generation Clock</strong> pattern addresses this — simply put, each leader is assigned a monotonically increasing generation number to indicate which “generation” of leader it is. Concepts like Raft’s “term” and ZAB’s “epoch” are implementations of this Generation Clock idea.</p><p>For clock skew, in a distributed environment, clock differences between nodes are inevitable. Under the Leader-Followers pattern, this problem is already largely minimized. Many systems choose to perform all operations on the leader, with leader-follower replication taking the form of replicating and replaying logs. In this way, clock issues generally don’t need to be considered. The only potential issue arises during leader failover, where the old leader and new leader might produce out-of-order data.</p><p>One solution to clock skew is to set up a dedicated synchronization service called an <strong>NTP service</strong>. However, this approach is not perfect — since it involves network operations, some error is inevitable. When relying on NTP to solve clock skew, the system design must tolerate very small timing errors.</p><p>Actually, beyond forcibly aligning clocks, there are simpler approaches to consider. First, let’s think about a question: do we really need to ensure messages are ordered by real-world physical time? Not really. What we need is a <strong>consistent, reproducible way to determine message order</strong> so that all nodes agree on the order. In other words, messages don’t have to follow physical chronological order, but the order determined by different nodes should be the same.</p><p>The <strong>Lamport Clock</strong> technique achieves this goal. Its logic is simple, as shown in the diagram:</p><p><img src="/en/img/lamport.png" alt="lamport stamp"></p><p>Operations on a local node increment the local stamp by 1. During network communication — for example, when node C receives data from node B — it compares its current stamp with B’s stamp + 1 and takes the larger value as its new stamp. This simple operation guarantees that the order of any two causally related operations (whether on the same node or communicated between nodes) is consistent across all nodes.</p><h2 id="Design-Principles-Summary"><a href="#Design-Principles-Summary" class="headerlink" title="Design Principles Summary"></a>Design Principles Summary</h2><p>Additionally, there are relatively simpler considerations that frequently arise in distributed system design — such as how to distribute data evenly across nodes. For this, we might need to find an appropriate shard key based on the business scenario, or find a suitable hash algorithm. There is also <strong>consistent hashing</strong> technology, which gives us more flexibility in controlling data distribution.</p><p>Another key consideration in distributed system design is how to measure system performance, with metrics including latency, throughput, availability, consistency, and scalability. These are easy to understand conceptually, but measuring them more comprehensively — especially observing them conveniently — is itself a large topic.</p><p>Source: <a href="https://lichuanyang.top/en/posts/45718/">https://lichuanyang.top/en/posts/45718/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/45718/</id>
    <link href="https://lichuanyang.top/en/posts/45718/"/>
    <published>2022-04-13T12:13:24.000Z</published>
    <summary>An exploration of common patterns in distributed system design, including sharding, replication, consistency models, and fault tolerance strategies.</summary>
    <title>Common Patterns in Distributed System Design</title>
    <updated>2026-06-27T03:48:16.252Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="computational-advertising" scheme="https://lichuanyang.top/en/tags/computational-advertising/"/>
    <category term="internet-advertising" scheme="https://lichuanyang.top/en/tags/internet-advertising/"/>
    <category term="rtb" scheme="https://lichuanyang.top/en/tags/rtb/"/>
    <content>
      <![CDATA[<p>In this article, I will introduce the current state of internet advertising and untangle the underlying logic of internet advertising, to help everyone better understand this industry.</p><span id="more"></span><h2 id="What-Is-Advertising"><a href="#What-Is-Advertising" class="headerlink" title="What Is Advertising"></a>What Is Advertising</h2><p>When discussing internet advertising, we must first start with what advertising itself is. Only by understanding the underlying logic of advertising can we better understand what problems exist in internet advertising, why programmatic advertising trading emerged, why various systems like ADX, SSP, DSP, DMP appeared, and other similar questions.</p><p>The advertising industry has actually existed since ancient times — from street vendors’ hawking in ancient times, to signs on buildings, to ads in print newspapers, magazines, TV, and other traditional media, and then to internet advertising, this industry has always evolved along with societal development.</p><p>For advertising, let’s first provide a very official, very standard definition: Advertising is a paid, comprehensive, persuasive, non-personal information dissemination activity about products (goods, services, and ideas) conducted through various media by identified sponsors.</p><p>More colloquially, in advertising transactions, there are three core participants: the media (the supply side), the advertisers (the demand side), and the audience (the media’s users). The media and advertisers are active participants, while the audience is a passive participant. Advertising is essentially a process of game theory among these three parties. Advertisers want more audience members to see their ads and achieve higher conversion rates; media want to maximize advertising revenue while minimizing disruption to users; and the audience — ordinary users — simply wants to avoid being bothered by ads they don’t want to see.</p><p>Next, let’s analyze the strategic considerations in this process. As everyone knows, if something is a zero-sum game for all participants, it will inevitably fall into cutthroat competition and be difficult to develop. Since the advertising industry has been able to sustain healthy development, it is mainly due to its impact outside the advertising ecosystem. So let’s analyze what external factors influence the advertising ecosystem.</p><p>Let’s first break down what each party considers. For the media, the main metrics are advertising revenue and product retention. For advertisers, it’s advertising costs and the revenue gained from advertising. As passive participants, the audience can do little during this process — they can only vote with their feet by leaving products with terrible ads. Among these, what lies outside the entire advertising ecosystem are two aspects: the revenue advertisers gain from advertising, and reducing the disruption advertising causes to the audience. In fact, improving these two points is the driving force behind the continuous development of the entire advertising industry.</p><p>Understanding this point helps us better understand the development history of internet advertising.</p><h2 id="The-Development-History-of-Internet-Advertising"><a href="#The-Development-History-of-Internet-Advertising" class="headerlink" title="The Development History of Internet Advertising"></a>The Development History of Internet Advertising</h2><h2 id="Early-Development-of-Internet-Advertising"><a href="#Early-Development-of-Internet-Advertising" class="headerlink" title="Early Development of Internet Advertising"></a>Early Development of Internet Advertising</h2><p>The earliest internet advertising was essentially a direct migration of traditional media advertising formats online. For example, in a newspaper, there would be a dedicated area for ads. The ads we used to see on websites like Sohu and Sina were also in this format. Advertisers would directly purchase a specific area on a webpage to display certain ad content.</p><p>Later, some people gradually noticed a key difference between internet advertising and traditional media: internet advertising content can vary from person to person. Printed newspapers look the same to everyone, but websites can easily show different ads to different groups, such as men and women. The media only needed to guarantee the advertiser a certain delivery volume and compensation methods when the delivery targets were not met. This is known as contract-based advertising.</p><p>A natural development trend of contract-based advertising is increasingly granular targeting — gender, age, location, interests, and hobbies can all become part of the contract terms. At the same time, more and more advertisers entered the scene. Both of these factors would cause the difficulty of fulfilling contracts to increase dramatically. Moreover, under such complex logic, the media also felt that a lot of their traffic was not being sold at the highest possible price.</p><p>In this situation, a new advertising format emerged: auction-based advertising. The supply side no longer guarantees volume through contracts, only guaranteeing the cost per unit of traffic. Each ad impression is decided based on the highest revenue principle. In this way, advertising gradually evolved into a “traffic trading” model, forming programmatic advertising trading.</p><h2 id="Programmatic-Advertising-Trading"><a href="#Programmatic-Advertising-Trading" class="headerlink" title="Programmatic Advertising Trading"></a>Programmatic Advertising Trading</h2><p>In the scenario of auction-based advertising, without contract constraints, a large number of media and advertisers entered a multi-party game environment, making the transaction process increasingly complex and giving rise to transaction methods such as RTB (Real-Time Bidding). Programmatic advertising trading emerged precisely in this context.</p><p>Meanwhile, with the development of technology, more data, deeper computation and prediction became possible, and all participants — advertisers, media, and related advertising agencies — needed richer functionality. The terms mentioned in our title — SSP, ADX, DSP, RTB — also gradually developed into very important systems in the advertising ecosystem under these circumstances.</p><p>Overall, programmatic advertising trading is very similar to real-time trading scenarios like stock trading — there is a trading venue (ADX), buyers (advertisers), and sellers (media), each making real-time buying and selling decisions within it. The only important difference is that for media, there is no opportunity to time the market; when traffic arrives, it must be sold immediately, with no way to hold it and wait for a good opportunity. This point is precisely the starting point for the development of various advertising-related technologies.</p><h2 id="Key-Issues-and-Components-of-Internet-Advertising"><a href="#Key-Issues-and-Components-of-Internet-Advertising" class="headerlink" title="Key Issues and Components of Internet Advertising"></a>Key Issues and Components of Internet Advertising</h2><p>As mentioned earlier, there are two driving forces behind the development of internet advertising: increasing the revenue advertisers gain from advertising, and reducing the disruption advertising causes to the audience. Both of these relate to advertising effectiveness, so let’s analyze what factors influence advertising effectiveness.</p><p>The lifecycle of an ad can roughly be broken down into these steps:</p><p>Exposure → Attention → Understanding → Acceptance → Retention → Decision</p><p>Simply put, the ad must first be displayed, then be seen by the audience, who must understand the message the ad is trying to convey. Furthermore, they must accept this message and retain it for some time, ultimately completing the product decision promoted by the ad (download, purchase, payment, etc.).</p><p>Next, let’s go through the specific impact of each step and what role internet technology can play.</p><p><strong>Exposure</strong>: This stage mainly depends on the physical properties of the ad placement. For example, an ad on an app’s splash screen versus an ad buried deep on some hidden page — their effectiveness will obviously differ greatly. This is the same in real-world advertising, and there isn’t much room for technological optimization here.</p><p><strong>Attention</strong>: This means getting users to notice the ad. It’s worth noting that “seeing” does not equal “noticing.” If the product design is unreasonable, users can easily overlook what’s right in front of them. For example, if a user comes to a page with a very specific task — say, downloading something — then forcing an ad into their download process will very likely cause the user to completely ignore it. Therefore, this step is closely related to product design. Additionally, this is a stage where technology can play an important role. By building a DMP (Data Management Platform) and combining various machine learning algorithms, ads can be delivered to users who are more likely to be interested, thereby increasing ad effectiveness.</p><p><strong>Understanding</strong>: The factor with the greatest impact at this step is the ad creative. Well-designed creative content helps users better understand the message.</p><p><strong>Acceptance</strong>: Simply put, this is about whether users can认可 the information or viewpoint your ad presents. On one hand, this relates to whether the creative is professional; on the other hand, it has a lot to do with the media’s own attributes. For example, the same information placed on DingXiang Doctor versus some Putian hospital website would have vastly different effectiveness.</p><p><strong>Retention and Attention</strong>: These two steps gradually move beyond the scope of the advertising ecosystem, but through quality creative design, appropriate scenarios, and precise audience targeting, they can still lay the groundwork for the final conversion.</p><p>Additionally, for stages that these technologies cannot directly intervene in, providing more complete data and more convenient testing methods can also bring significant help to ad performance.</p><p>Next, let’s look separately from the perspective of media and advertisers to understand their respective needs, and briefly examine how platforms like SSP and DSP work. Due to space constraints, this part will not be detailed here. If you’re interested, we can explore each one in future articles.</p><p>The platform serving the media is what’s commonly known as SSP, whose main function is to help media increase revenue. Some basic features include allowing media to configure ad placements and specifying dimensions like width and height. For increasing revenue, the main approaches are matching the right audience with the right ads, optimizing pricing strategies, and attempting to control IT costs such as network expenses.</p><p>The counterpart to SSP is DSP, which serves the advertiser side. The basic function of DSP is to allow advertisers to set up delivery plans — for example, over what time period, to what type of users, with what total budget. The value the platform provides is mainly helping advertisers spend less to achieve better ad results.</p><p>Even today, every module involved in the advertising ecosystem is still undergoing continuous iteration, but the underlying principle remains the same: the key driving force is making ad delivery more effective while providing users with a better ad browsing experience. Only in this way can the entire industry develop in a healthy direction.</p><p>Original article: <a href="https://lichuanyang.top/posts/27934/">https://lichuanyang.top/posts/27934/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/27934/">https://lichuanyang.top/en/posts/27934/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/27934/</id>
    <link href="https://lichuanyang.top/en/posts/27934/"/>
    <published>2022-03-07T08:28:58.000Z</published>
    <summary>Starting from the essence of advertising, systematically untangle the evolution of internet advertising, explaining core concepts like SSP, DSP, RTB, ADX and the underlying logic of programmatic trading.</summary>
    <title>What Are SSP, DSP, RTB, and ADX? Understanding the Concepts and Evolution of Internet Advertising</title>
    <updated>2026-06-27T03:57:34.899Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Database" scheme="https://lichuanyang.top/en/categories/Database/"/>
    <category term="database-transactions" scheme="https://lichuanyang.top/en/tags/database-transactions/"/>
    <category term="database" scheme="https://lichuanyang.top/en/tags/database/"/>
    <category term="acid" scheme="https://lichuanyang.top/en/tags/acid/"/>
    <category term="transaction-isolation-levels" scheme="https://lichuanyang.top/en/tags/transaction-isolation-levels/"/>
    <content>
      <![CDATA[<p>Recently, I read Zhou Zhiming’s book “Phoenix Architecture” and gained deeper insights into many aspects of technology. I plan to make some summaries. Today, I’ll start with the section on transactions, combining content from the book with my own understanding, aiming to explain local transactions clearly and thoroughly. If there are any inaccuracies, I welcome corrections.</p><span id="more"></span><p>A transaction ensures that data in the database remains in a valid state. Through continuous CRUD operations, the database transitions from one correct state to another, without exposing intermediate “incorrect” states to the outside world.</p><p>A common example: when A has 100 yuan and B has 100 yuan, and A wants to transfer 10 yuan to B, there will inevitably be an intermediate state where A has transferred out 10 yuan but it hasn’t reached B yet. A transaction ensures that users can only perceive two states — A:100, B:100 and A:90, B:110 — and never the strange intermediate state of A:90, B:100, etc.</p><p>This description actually corresponds to the Consistency property in ACID. While ACID is a popular term, A, C, I, and D are not equally weighted concepts. Simply put, A, I, and D are methods, while C is the goal. In other words, by implementing Atomicity, Durability, and Isolation, you achieve Consistency — and thus achieve a transaction.</p><p>Next, let’s look at how each of A, I, and D can be implemented.</p><h2 id="Atomicity-and-Durability"><a href="#Atomicity-and-Durability" class="headerlink" title="Atomicity and Durability"></a>Atomicity and Durability</h2><p>Atomicity and Durability face many of the same implementation challenges, so they’re introduced together.</p><p>Let’s review the basic concepts: Atomicity means all operations within a transaction either all succeed or all fail; Durability means that once committed, completed operations are not lost.</p><p>It’s worth noting that “simply implementing atomicity and durability” is not inherently difficult. The real question is “how to implement atomicity and durability with high performance” (the same applies to Isolation — only high-performance isolation is meaningful).</p><p>A key point is that writing to disk is a very expensive operation, so there is usually a memory buffer. Data to be written to disk is first written to the buffer and then flushed to disk at an appropriate time. If the system fails after a transaction is committed but before the data is flushed to disk, this unflushed data will naturally be lost, and the database loses its durability. A natural solution is to force a disk flush when the transaction commits. Is this feasible? Of course it is. But the problem is that it impacts performance. System failures are rare events, and handling this rare scenario effectively means every operation incurs additional overhead.</p><p>In practice, a common approach to this problem is to use a commit log — that is, before actually writing data, first record all information about modifications in a log. If the above failure occurs, the system can recover data based on the commit log upon restart. Since writing this log is a sequential disk write operation, its performance is far better than random disk writes, so this approach has no performance issues. After the data is truly written, a marker is added to indicate that this log entry has been persisted.</p><p>Now, let’s consider whether the commit log has room for optimization. Of course it does. A major drawback of the commit log is that all actual disk operations must occur after the transaction commits. If a transaction is very large, it will occupy a significant amount of memory buffer, which also impacts system performance. The improvement is the write-ahead log (WAL) mechanism — a topic I’ve covered in a previous article (<a href="https://lichuanyang.top/posts/3914/">https://lichuanyang.top/posts/3914/</a>). WAL is very similar to the commit log — it also sequentially writes a log file, with the only difference being that WAL allows writing before the transaction commits. MySQL’s redo log is a typical implementation of write-ahead logging.</p><p>At this point, let’s pause and review the above content. You’ll notice that most of it actually discusses durability, because with these mechanisms, atomicity comes naturally — if the commit log is written, the transaction is effectively complete; if the commit log is not written, the transaction effectively never happened. However, with write-ahead logging, the situation is different: a transaction involves multiple disk writes, so atomicity is no longer guaranteed. Therefore, additional mechanisms are needed to ensure atomicity. Undo logs are a typical approach for this purpose. Before writing data changes to disk, the undo log must first be recorded, noting which data was modified, the original value, and the new value, so that during transaction rollback or crash recovery, data changes written earlier can be undone based on the undo log.</p><p>In MySQL, this is essentially what happens: redo logs and undo logs are used together to implement efficient and reliable durability and atomicity.</p><h2 id="Isolation-and-Isolation-Levels"><a href="#Isolation-and-Isolation-Levels" class="headerlink" title="Isolation and Isolation Levels"></a>Isolation and Isolation Levels</h2><p>How to implement isolation between transactions? A natural approach is locking, and this is indeed how conventional databases are implemented. Generally, there are several types of locks: read locks (also called shared locks), write locks (also called exclusive locks), and range locks.</p><p>For a given piece of data, only one transaction can hold a write lock; different transactions can simultaneously hold read locks. Once a data item has a read lock, a write lock cannot be added, and once it has a write lock, a read lock cannot be added. Range locks apply a write lock to a range, preventing data writes within that range.</p><p>We know that databases have four common isolation levels: Serializable, Repeatable Read, Read Committed, and Read Uncommitted. The difference between them is essentially the granularity of locking.</p><p>If we add all possible locks to all operations, the result is effectively serializable execution. This approach provides excellent isolation but terrible performance, so it’s generally not used.</p><p>Repeatable Read adds read and write locks to all involved data and holds them until the transaction ends, but does not add range locks. This can lead to phantom reads — if two range queries are executed within a transaction, and new data is inserted between them, the two queries may return inconsistent results.</p><p>Read Committed differs from Repeatable Read in that its read locks are released immediately after the query completes. This means that during transaction execution, data that has been read can be modified by other transactions, leading to non-repeatable reads.</p><p>Under Read Uncommitted, no read locks are added at all. The problem this causes is that read operations don’t request read locks, which actually allows reading data that has write locks from other transactions — leading to dirty reads.</p><p>Ultimately, isolation and performance are contradictory requirements. The more locks you add, the better the isolation, but the worse the performance. We need to decide the appropriate locking level based on the actual use case.</p><p>Another approach is to look for alternatives beyond locking. Considering the 80&#x2F;20 principle, can we sacrifice 20% of performance to solve 80% of the problem? Specifically, isolation problems can be simplified into two scenarios: one read transaction plus one write transaction, and two write transactions. In most cases, the read-plus-write scenario is more common, so we look for ways to solve phantom reads in that scenario. Many of you have probably already guessed — this is MVCC (Multi-Version Concurrency Control). There is abundant information about MVCC online, so I won’t go into further detail here.</p><p>After the above introduction to the various transaction properties, I believe you now have a deep understanding of local transactions. If you have any questions, feel free to leave a comment. In the next article, I will discuss distributed transactions. If you’re interested, you can follow my personal blog, Zhihu, or WeChat official account for updates~</p><p>Source: <a href="https://lichuanyang.top/en/posts/7774/">https://lichuanyang.top/en/posts/7774/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/7774/</id>
    <link href="https://lichuanyang.top/en/posts/7774/"/>
    <published>2022-02-07T08:40:02.000Z</published>
    <summary>A comprehensive exploration of database transactions and ACID properties, covering redo log, undo log, and isolation levels.</summary>
    <title>From Redo Log and Undo Log to Isolation Levels: A Deep Dive into Database Transactions and ACID</title>
    <updated>2026-06-27T03:50:56.858Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="continuous-integration" scheme="https://lichuanyang.top/en/tags/continuous-integration/"/>
    <category term="kubernetes" scheme="https://lichuanyang.top/en/tags/kubernetes/"/>
    <content>
      <![CDATA[<p>Compared with large manufacturers, many start-up companies have a very important disadvantage that their infrastructure is imperfect and they do not have a variety of complete tools. Therefore, I plan to sort out how to build a development process with a good experience based on the capabilities provided by the open source community with as little operation and maintenance and development costs as possible.</p><span id="more"></span><p>First, let’s sort out the necessary steps in the entire development process. I will briefly list them, including project creation, development, code review, deployment of test environment, deployment of grayscale and online environments, viewing online monitoring, and logs. , and troubleshooting problems, etc.</p><p>Based on this entire process chain, let’s take a look at what aspects we need to ensure in order to improve development and operation and maintenance efficiency as much as possible, and what low-cost solutions can be found in these aspects.</p><p>There are a few points:</p><ul><li>Quickly create a project and add necessary web, monitoring, log and other components;</li><li>Improve the development efficiency of some conventional code development, such as database addition, deletion, modification and query;</li><li>Simplify the process from packaging to deployment, and the test environment can be deployed automatically;</li><li>A complete set of monitoring and alarms can be easily added to each service, including common machine, JVM, interface level indicators, and business-customized indicators;</li><li>There is a place to centrally view the logs of each node;</li><li>When necessary, powerful tools can be used on online nodes to help troubleshoot problems.</li></ul><p>Next, let’s take a look at how to solve these problems respectively.</p><h2 id="Deployment-environment"><a href="#Deployment-environment" class="headerlink" title="Deployment environment"></a>Deployment environment</h2><p>First of all, in the title of this article, it is actually assumed that the deployment environment is kubernetes, so let me first talk about why kubernetes is the only option. Just flip through any introduction to kubernetes, and we can see many explanations of its advantages, so we won’t go into details here. The most critical one, and the benefit that everyone can experience personally, is that it provides a complete automatic expansion and contraction mechanism. It is very simple to set an expected value for CPU consumption, and the k8s cluster can adjust the number of pods based on the current CPU load. I compared the data before and after we enabled automatic expansion and contraction. The daily machine cost can differ by nearly 600 US dollars, accounting for more than 1&#x2F;3 of the total cost.</p><p>Since the operation and maintenance cost of k8s itself is very high, it is recommended to directly purchase the services of a cloud service provider. Both AWS and Alibaba Cloud provide very complete k8s cluster functions.</p><p>In addition, it is recommended to use kuboard (<a href="https://kuboard.cn/">https://kuboard.cn/</a>) this tool to manage k8s. kuboard is a graphical k8s management tool, including common operations such as deployment, configuration, expansion, and logging in to pods, all of which can be operated on a graphical interface and is very convenient to use.</p><h2 id="Project-Development"><a href="#Project-Development" class="headerlink" title="Project Development"></a>Project Development</h2><p>Generally, you can create a project based on spring initializr. For common configurations such as logging and monitoring, it is recommended to compile a list of best practices and create a template project. New projects can be created based on this template project, using mvn archetype or similar tools to make the process smoother.</p><p>As for some commonly used basic codes such as databases and caches, I feel that Spring Family Bucket is easy enough to use.</p><p>For code review, we can use GitLab’s webhook to automatically send notifications to the project team when the code is submitted.</p><h2 id="Packaging-Deployment"><a href="#Packaging-Deployment" class="headerlink" title="Packaging &amp; Deployment"></a>Packaging &amp; Deployment</h2><p>For springboot projects, you can use buildpacks to create docker images without having to consider the details of the docker file. For specific buildpacks, we use paketo buildpacks. Others such as cloudfoundry and heroku are similar. Currently, we have not studied the specific requirements for these packages.</p><p>Next, we can still use gitlab’s webhook to trigger a Jenkins build task. In the Jenkins build task, we can complete the operations of packaging and deploying to the kubernetes test cluster.</p><h2 id="Monitoring-Alarming"><a href="#Monitoring-Alarming" class="headerlink" title="Monitoring &amp; Alarming"></a>Monitoring &amp; Alarming</h2><p>It is recommended to use the prometheus + grafana package. Regarding the use of prometheus and the configuration under the springboot project, you can refer to my previous article h[ttps:&#x2F;&#x2F;lichuanyang.top&#x2F;posts&#x2F;28288&#x2F;](<a href="https://lichuanyang/">https://lichuanyang</a>. top&#x2F;posts&#x2F;28288&#x2F;) .</p><p>On k8s, you can install a weave cloud agent, and then configure the automatic crawling of the prometheus interface.</p><p>In grafana, you can directly write promeql configuration monitoring reports. In addition, on the Grafana official website, there are a large number of charts shared by others that can be used directly.</p><p>In grafana, you can also configure alarms with various custom rules. If you use Feishu, you can easily use Feishu as the alarm channel for grafana by configuring grafana assistant in Feishu.</p><h2 id="log"><a href="#log" class="headerlink" title="log"></a>log</h2><p>You can use loki (<a href="https://grafana.com/oss/loki/">https://grafana.com/oss/loki/</a>) as a tool for log collection and query. Loki can be considered as a lightweight ELK, and its maintenance cost will be much lower than ELK.</p><h2 id="Troubleshooting"><a href="#Troubleshooting" class="headerlink" title="Troubleshooting"></a>Troubleshooting</h2><p>For java projects, using the artifact arthas can solve most problem troubleshooting needs. For arthas access, you can use arthas springboot starter. For pods on k8s, if the environment can be connected to the office environment, you can use the port forward function of k8s to forward the arthas port to the local. Of course, if you do this, be sure to control permissions.</p><h2 id="Canary-deployment"><a href="#Canary-deployment" class="headerlink" title="Canary deployment"></a>Canary deployment</h2><p>Regarding the role and implementation of canary deployment, you can refer to my other article <a href="https://lichuanyang.top/posts/30764/">https://lichuanyang.top/posts/30764/</a></p><p>To sum up, for operation and maintenance, you only need to maintain some infrastructure such as gitlab, kuboard, prometheus, grafana, loki, etc., and they are basically tools that are relatively simple to maintain. On this basis, with the help of reasonable processes and techniques, we can achieve a very good development experience.</p><p>Original address: <a href="https://lichuanyang.top/en/posts/40964/">https://lichuanyang.top/en/posts/40964/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/40964/</id>
    <link href="https://lichuanyang.top/en/posts/40964/"/>
    <published>2022-01-26T07:19:32.000Z</published>
    <summary>Practical experience using Kubernetes in startup Java projects with low infrastructure costs, covering CI/CD, deployment, and monitoring setup.</summary>
    <title>Practical experience using kubernetes in java projects with low learning costs</title>
    <updated>2026-06-27T03:50:56.861Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <content>
      <![CDATA[<p>2021 was a year of upheaval for me personally. A job I had planned to pursue long-term — one with excellent comfort and promising prospects — suddenly collapsed due to certain reasons. Consequently, I faced a critical crossroads regarding my future direction. At least so far, I feel I’ve made some correct choices. I’ve also made good progress on the personal growth goals I set at the beginning of the year. So I’m approaching this year-end review with a reasonably positive mindset.</p><span id="more"></span><h2 id="Career-Review"><a href="#Career-Review" class="headerlink" title="Career Review"></a>Career Review</h2><p>The biggest change in 2021 came from work. Since the industry I was in disappeared from the spotlight overnight, my plans to develop at one company for the long haul fell through, and I was forced to restart my career planning. A key consideration was whether to join a big company or a small company. The reason I mentioned reaching a critical crossroads is that at this age, whichever path I choose, there’s essentially no turning back. The capability requirements for a senior employee at big companies versus small companies are vastly different. At this stage, you no longer have the freedom to jump between big and small companies like you could in your early career after graduation.</p><p>To make this decision, I asked myself several questions:</p><p>Would I grow faster personally at a big company compared to a small one?</p><p>Would life at a big company be more comfortable than at a small one?</p><p>Will I need another big company experience as credentials in the future?</p><p>Could I earn more income at a big company?</p><p>After careful consideration, the answer to all these questions was actually no. Personal growth depends on the individual — I’m fundamentally not someone easily influenced by my environment. Regarding work-life balance, in theory it shouldn’t have much to do with company size, it just happens that several big companies in China aren’t great at this. On the credentials front, since I already have big tech experience, plus my educational background and other prior experience, I don’t think I’d lose out in that regard. Also, based on this job search and my experience interviewing candidates over the past two years, I’ve come to believe that the substance of your projects matters more than which company you were at. As for income, it depends on the macro environment, the company’s trajectory, the boss’s style, and luck — it generally doesn’t correlate much with company size.</p><p>After all this deliberation, I realized that the only advantage of joining a big tech company was that it would look good on paper. So I made my decision: completely abandon the big company idea and instead seek out promising startup companies with good culture and outlooks. Speaking of which, I’m sometimes deeply grateful for what seems like fate’s grace — it’s as if the universe always presents me with suitable options at key moments. In reality, though, I mainly have to thank the headhunter who provided information and support throughout the process, leading me to a new company that satisfied me on every front. I’ve been at the new company for almost four months now, and during this period, the room for doing meaningful work, personal growth, WLB, and financial returns have all exceeded expectations.</p><h2 id="Life-Changes"><a href="#Life-Changes" class="headerlink" title="Life Changes"></a>Life Changes</h2><p>With the new work schedule, I also have more time with my daughter. The little girl has recently entered a language explosion phase, saying new words almost every day, and my sense of life happiness has reached a very high level.</p><h2 id="Technical-Growth"><a href="#Technical-Growth" class="headerlink" title="Technical Growth"></a>Technical Growth</h2><p>Additionally, I set some personal growth goals at the beginning of each year. At the start of 2021, my key goals were to expand and solidify my capability circle — striving to master new skills and reinforce existing ones. Specifically, this included improving technical and management skills related to my day job, enhancing business acumen and investment ability, and building some foundational skills.</p><p>On the professional front, I read Zhou Zhiming’s “Phoenix Architecture,” which played a key role in helping me build a more complete knowledge system and fill in some knowledge gaps. I studied Wang Zheng’s design patterns column on Geek Time, gained deeper understanding of design patterns, and gave a design patterns talk within my company. I also did a concentrated study of Elasticsearch, a technology I’d been using for a long time without seriously understanding.</p><p>On management, I learned some theoretical knowledge this year and have been working on applying it to practical work, with decent results.</p><p>On investments, I actually made some very mistaken attempts this year. Starting from early in the year, I tried to analyze company fundamentals myself to find suitable investment targets, reading many books and studying the experiences of notable investors. My current conclusion is that the barrier to this is extremely high — it’s unrealistic to achieve professional-level investment results through self-study alone. Based on this experience, my investment returns turned negative for the first time this year. Going forward, I’ll abandon individual stock investing and shift entirely to fund and index dollar-cost averaging. However, I won’t stop learning about business and investment knowledge, because I’ve realized that this knowledge can meaningfully transform one’s way of thinking, and the gains are well worth it.</p><p>Regarding foundational capabilities, I focused this year on cultivating what I call “clarity power.” This concept comes from the book “Cognitive Awakening” — the idea being that our fears are often rooted in “ambiguity,” meaning we haven’t clearly figured out what we’re actually facing. Once we have a clear understanding of a situation, we realize it’s not that difficult. The first time I encountered this concept, I immediately recognized that I’d always had a significant problem in this area, so I began deliberate practice. The results have been quite good — whether in work or life matters, I feel much more capable of handling things smoothly.</p><h2 id="2022-Outlook"><a href="#2022-Outlook" class="headerlink" title="2022 Outlook"></a>2022 Outlook</h2><p>In summary, this year has been a year of critical decisions for me, and I’m steadily moving toward my life goals. I’m grateful to everyone who has helped me along the way. Here’s to continuing to give it my all in 2022.</p><p>Source: <a href="https://lichuanyang.top/en/posts/2345/">https://lichuanyang.top/en/posts/2345/</a></p><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-What’s-the-path-forward-for-mid-career-engineers"><a href="#Q-What’s-the-path-forward-for-mid-career-engineers" class="headerlink" title="Q: What’s the path forward for mid-career engineers?"></a>Q: What’s the path forward for mid-career engineers?</h3><p>The path doesn’t depend on company size — it depends on your capability circle. At the mid-to-late career stage, what matters more than “which company to join” is: what domain you have unique insights in, what level of technical problems you can solve, and whether your experience has crystallized into transferable methodologies. The author’s choice — letting go of big-tech prestige to seek startups with good culture and promising prospects — offers one viable path. But it’s not the only one. The key is to actively think about what you want, not passively wait for opportunities to land.</p><h3 id="Q-How-should-technical-people-respond-to-industry-changes"><a href="#Q-How-should-technical-people-respond-to-industry-changes" class="headerlink" title="Q: How should technical people respond to industry changes?"></a>Q: How should technical people respond to industry changes?</h3><p>In one phrase: expand your capability circle. An industry can vanish overnight (as the tutoring industry did for the author), but if you’ve been consistently broadening your capabilities, you gain the mobility to move across industries. Concretely: maintain technical sharpness (read seminal books like “Phoenix Architecture”), cultivate transferable foundational skills (like “clarity power”), and never anchor your sense of professional security to a single company or industry.</p><h3 id="Q-Big-company-or-startup-—-which-is-better-for-senior-engineers"><a href="#Q-Big-company-or-startup-—-which-is-better-for-senior-engineers" class="headerlink" title="Q: Big company or startup — which is better for senior engineers?"></a>Q: Big company or startup — which is better for senior engineers?</h3><p>It depends on what you need most at this stage. Big companies offer platform credentials and resource leverage, but personal growth pace and work-life balance aren’t guaranteed. Startups offer broader scope and faster decision-making, but stability is lower. The author’s conclusion after comparison: personal growth comes from yourself, not company size; if you already have big-tech experience, credentials matter less; and income depends more on the macro environment and luck than company size. The core advice: clarify your current-stage selection criteria before deciding, because at this stage of your career, there’s rarely a path back.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/2345/</id>
    <link href="https://lichuanyang.top/en/posts/2345/"/>
    <published>2022-01-04T13:49:57.000Z</published>
    <summary>Year-end review for 2021: career transition amid industry upheaval, progress on personal growth goals, and reflections on the future.</summary>
    <title>2021 in Upheaval — A Mid-Career Engineer's Year-End Review</title>
    <updated>2026-06-27T02:41:01.235Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Cloud Native" scheme="https://lichuanyang.top/en/categories/Cloud-Native/"/>
    <category term="cloud-native" scheme="https://lichuanyang.top/en/tags/cloud-native/"/>
    <category term="continuous-integration" scheme="https://lichuanyang.top/en/tags/continuous-integration/"/>
    <category term="canary" scheme="https://lichuanyang.top/en/tags/canary/"/>
    <content>
      <![CDATA[<p>Canary release is actually a process well-suited for cloud-native environments, so I believe many people have the need to perform canary releases under Kubernetes. In this article, we introduce a very simple approach to canary release.</p><span id="more"></span><h2 id="What-is-Canary-Release"><a href="#What-is-Canary-Release" class="headerlink" title="What is Canary Release"></a>What is Canary Release</h2><p>First, let me introduce what a canary release is. The name “canary” originates from the fact that miners discovered canaries are very sensitive to gas. Before descending into the mine, miners would send a canary down first. If the canary stopped singing, it indicated a high concentration of gas.</p><p>In the context of systems, it means that after a release begins, a new version of the application is started first, but traffic is not switched over directly. Instead, testers perform online testing on the new version. The newly started application is our “canary.” After testing on the canary shows no issues, the production traffic is then switched to the new version.</p><p>To be more specific, a domain name can be mapped to two groups of servers — one group is the production environment, and the other is the canary environment.</p><p>If we are not deploying with Kubernetes but directly deploying to multiple servers, canary deployment is very simple — just designate one or more machines as the canary.</p><p>However, in a Kubernetes environment, since Kubernetes manages the deployment process, we need to do something different to achieve this effect.</p><h2 id="Canary-Release-Approach-in-Kubernetes"><a href="#Canary-Release-Approach-in-Kubernetes" class="headerlink" title="Canary Release Approach in Kubernetes"></a>Canary Release Approach in Kubernetes</h2><p>A very simple approach is to create two Deployments that are associated with the same Service through labels. This distributes traffic from the same Service across two groups of containers. The two Deployments can be deployed independently, allowing different versions of images to be deployed.</p><h2 id="Deployment-Configuration-Example"><a href="#Deployment-Configuration-Example" class="headerlink" title="Deployment Configuration Example"></a>Deployment Configuration Example</h2><p>For example, here are two Deployments:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-deployment</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">3</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">nginx:1.7.9</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-service</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx-port</span></span><br><span class="line">    <span class="attr">protocol:</span> <span class="string">TCP</span></span><br><span class="line">    <span class="attr">port:</span> <span class="number">80</span></span><br><span class="line">    <span class="attr">nodePort:</span> <span class="number">32600</span></span><br><span class="line">    <span class="attr">targetPort:</span> <span class="number">80</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">NodePort</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-deployment-canary</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">    <span class="attr">track:</span> <span class="string">canary</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">      <span class="attr">track:</span> <span class="string">canary</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">track:</span> <span class="string">canary</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">nginx:1.8.0</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>The Service is only configured in the first Deployment, with a selector rule of <code>app: nginx</code>. Both Deployments have the <code>app: nginx</code> label applied.</p><p>This achieves the effect we described.</p><h2 id="Limitations"><a href="#Limitations" class="headerlink" title="Limitations"></a>Limitations</h2><p>Of course, this approach only implements a very simple canary release process and cannot perform more granular routing, such as gray releases based on user information. For the same user, one request might go to the canary while the next goes to the production environment. If more fine-grained gray release rules are needed, consider using tools like Spring Cloud, Istio, etc.</p><h2 id="FAQ"><a href="#FAQ" class="headerlink" title="FAQ"></a>FAQ</h2><h3 id="Q-What’s-the-difference-between-canary-release-blue-green-deployment-and-rolling-updates"><a href="#Q-What’s-the-difference-between-canary-release-blue-green-deployment-and-rolling-updates" class="headerlink" title="Q: What’s the difference between canary release, blue-green deployment, and rolling updates?"></a>Q: What’s the difference between canary release, blue-green deployment, and rolling updates?</h3><ul><li><strong>Rolling Update</strong>: Replaces old Pods batch by batch, with the new version going live gradually. It’s the simplest, but during the brief period when old and new versions coexist, traffic is mixed, and rollback can only be done in reverse batch order.</li><li><strong>Blue-Green Deployment</strong>: Prepare a complete “green” environment with the new version, then switch all traffic once testing passes. Fast switching and fast rollback, but requires double the resources.</li><li><strong>Canary Release</strong>: Route a small portion of traffic to the new version (the canary) first, then gradually scale up after verification. The safest approach, but requires more fine-grained traffic control capabilities.</li></ul><p>These three approaches are not mutually exclusive: canary can be seen as a “gradual version” of blue-green, and they are often combined in real-world projects.</p><h3 id="Q-Can-traffic-be-precisely-controlled-with-this-dual-Deployment-approach"><a href="#Q-Can-traffic-be-precisely-controlled-with-this-dual-Deployment-approach" class="headerlink" title="Q: Can traffic be precisely controlled with this dual-Deployment approach?"></a>Q: Can traffic be precisely controlled with this dual-Deployment approach?</h3><p>Traffic percentages cannot be precisely controlled. The Service balances load via kube-proxy’s iptables&#x2F;ipvs rules, defaulting to round-robin. The traffic ratio between the two Deployments roughly approximates the replica count ratio. For example, if you deploy 3 old Pods + 1 new Pod, about 25% of traffic goes to the new version — but this is only a statistical approximation, not precise control.</p><p>If you need precise traffic percentages (e.g., strictly 10% to canary), you’ll need an Ingress Controller (such as Nginx Ingress’s canary annotations) or a Service Mesh (such as Istio’s VirtualService).</p><h3 id="Q-What-if-I-want-to-route-by-user-dimension-for-canary-releases"><a href="#Q-What-if-I-want-to-route-by-user-dimension-for-canary-releases" class="headerlink" title="Q: What if I want to route by user dimension for canary releases?"></a>Q: What if I want to route by user dimension for canary releases?</h3><p>The approach described in this article is based on <strong>traffic-percentage-based</strong> simple canary releases and cannot route by user dimension (e.g., VIP users to the new version, internal employees to the new version). If you need user-dimension routing, consider:</p><ul><li><strong>Istio &#x2F; Linkerd</strong>: Route rules based on HTTP Headers (e.g., Cookie, User-Agent), enabling “requests with userId&#x3D;xxx go to canary”</li><li><strong>Spring Cloud Gateway</strong>: Route based on request parameters at the gateway layer</li><li><strong>Nginx Ingress canary-by-header</strong>: Route to the canary service based on specific Header values</li></ul><p>Original article: <a href="https://lichuanyang.top/posts/30764/">https://lichuanyang.top/posts/30764/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/30764/">https://lichuanyang.top/en/posts/30764/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/30764/</id>
    <link href="https://lichuanyang.top/en/posts/30764/"/>
    <published>2021-12-23T10:55:20.000Z</published>
    <summary>A simple approach to canary release in Kubernetes, reducing deployment risk by gradually routing traffic to new versions.</summary>
    <title>A Canary Release Approach in Kubernetes</title>
    <updated>2026-06-27T02:42:06.579Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Cloud Native" scheme="https://lichuanyang.top/en/categories/Cloud-Native/"/>
    <category term="monitoring" scheme="https://lichuanyang.top/en/tags/monitoring/"/>
    <category term="cloud-native" scheme="https://lichuanyang.top/en/tags/cloud-native/"/>
    <category term="prometheus" scheme="https://lichuanyang.top/en/tags/prometheus/"/>
    <category term="grafana" scheme="https://lichuanyang.top/en/tags/grafana/"/>
    <category term="prometheus-tutorial" scheme="https://lichuanyang.top/en/tags/prometheus-tutorial/"/>
    <content>
      <![CDATA[<p>As the “default” monitoring system in the cloud-native ecosystem, Prometheus is gaining increasing attention. Today, we’ll write a tutorial about Prometheus’s design philosophy, examining how it uses very simple designs to support such complex functionality.</p><span id="more"></span><p>First, let’s think about what the challenges would be in building a monitoring system similar to Prometheus:</p><ul><li>Each service has different monitoring requirements. How should a monitoring system design its data model to balance ease of use and generality?</li><li>How should large volumes of data be stored?</li><li>How can various complex reports be generated?</li><li>…</li></ul><p>With these questions in mind, let’s look at how Prometheus is designed.</p><h2 id="History"><a href="#History" class="headerlink" title="History"></a>History</h2><p>Let’s start with the history. Prometheus was originally developed by SoundCloud and later donated to the open-source community. In 2016, it joined CNCF (Cloud Native Computing Foundation). Prometheus is CNCF’s second project, second only to Kubernetes. As you can imagine, Prometheus plays a vital role in the entire cloud-native ecosystem. It has gradually become the de facto standard for monitoring systems in cloud-native environments.</p><h2 id="Core-Design-Philosophy"><a href="#Core-Design-Philosophy" class="headerlink" title="Core Design Philosophy"></a>Core Design Philosophy</h2><p>For a monitoring system, there are three core problems to solve:</p><ol><li>How monitoring metrics are represented</li><li>How to collect and store metrics</li><li>How to use metrics to generate reports</li></ol><p>For these three questions, Prometheus offers very elegant solutions.</p><h2 id="Data-Model"><a href="#Data-Model" class="headerlink" title="Data Model"></a>Data Model</h2><p>Prometheus’s data model is, in short, “time-series” metric data. A metric is a measurement of data, and time-series means these metrics continuously generate data points at different time points.</p><p>Each metric has a unique name identifier and can have multiple labels set for filtering and aggregation. The format is as follows:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;metric name&gt;&#123;&lt;label name&gt;=&lt;label value&gt;, ...&#125;</span><br></pre></td></tr></table></figure><p>This way, for any business, monitoring data can be designed into a unified metric format. This allows Prometheus to keep its approach simple — it only needs to handle this one data format. At the same time, it is flexible enough to accommodate diverse business scenarios.</p><p>Prometheus provides four core metric types: counter, gauge, histogram, and summary. However, their differences only manifest on the client side and in PromQL. As of now (2021.11), different metric types don’t differ on the Prometheus server side.</p><h2 id="Data-Collection-and-Storage"><a href="#Data-Collection-and-Storage" class="headerlink" title="Data Collection and Storage"></a>Data Collection and Storage</h2><p>The Prometheus server periodically scrapes data from HTTP endpoints exposed by the services being monitored — this is a typical pull model.</p><p>Compared to the push model, the pull model has some advantages, such as making it easier to detect whether a specific node is functioning properly, and easier to debug locally. Of course, for a monitoring system, whether to use push or pull is not a fundamental issue.</p><p>Prometheus data is typical time-series data. Prometheus itself stores data on local disk. Note that local storage is not replicable and cannot form a cluster. If the local disk or node fails, storage cannot be scaled or migrated. Therefore, local storage should generally be treated only as a short-term sliding window for recent data.</p><p>Regarding persistent storage, Prometheus doesn’t actually try to solve this problem itself. Instead, it defines standard read&#x2F;write interfaces, allowing data to be stored on any third-party storage system.</p><h2 id="Generating-Reports"><a href="#Generating-Reports" class="headerlink" title="Generating Reports"></a>Generating Reports</h2><p>Prometheus defines the powerful PromQL language, which can satisfy various complex query scenarios. For details, refer to <a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">https://prometheus.io/docs/prometheus/latest/querying/basics/</a></p><h2 id="Ecosystem"><a href="#Ecosystem" class="headerlink" title="Ecosystem"></a>Ecosystem</h2><p>The development of any open-source project depends on the growth of its ecosystem. Prometheus now has a very mature ecosystem. In mainstream programming languages like Java, Go, and Python, there are complete client libraries available. In Spring, you can easily add instrumentation to various components — we’ll cover this in detail in the practical section below. In Kubernetes, you can easily configure automatic scraping of Prometheus metrics from each node. With tools like Grafana, you can also configure a wide variety of dashboards and reports.</p><h2 id="Hands-On-Practice"><a href="#Hands-On-Practice" class="headerlink" title="Hands-On Practice"></a>Hands-On Practice</h2><p>In the next part of this tutorial, we’ll use a Spring Boot project as an example to see Prometheus in action.</p><p>The core idea is to use Spring Actuator to configure monitoring for a Spring Boot application and expose it in Prometheus format.</p><p>First, add the dependencies:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)</span><br><span class="line">implementation(&quot;io.micrometer:micrometer-registry-prometheus&quot;)</span><br></pre></td></tr></table></figure><p>Then add the Spring configuration:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">management:</span><br><span class="line">  endpoints:</span><br><span class="line">    web:</span><br><span class="line">      exposure:</span><br><span class="line">        include: &quot;prometheus&quot;</span><br><span class="line">  metrics:</span><br><span class="line">    distribution:</span><br><span class="line">      sla:</span><br><span class="line">        http:</span><br><span class="line">          server:</span><br><span class="line">            requests: &quot;100ms,150ms,250ms,500ms,1s&quot;</span><br><span class="line">      percentiles-histogram:</span><br><span class="line">        http:</span><br><span class="line">          server:</span><br><span class="line">            requests: true</span><br><span class="line">    web:</span><br><span class="line">      server:</span><br><span class="line">        request:</span><br><span class="line">          autotime:</span><br><span class="line">            enabled: true</span><br><span class="line">    export:</span><br><span class="line">      prometheus:</span><br><span class="line">        enabled: true</span><br><span class="line">    tags:</span><br><span class="line">      application: name</span><br></pre></td></tr></table></figure><p>This configuration does several things: exposes data in Prometheus format, automatically adds histogram monitoring for HTTP requests, and adds an <code>application</code> identifier that appears as a label in all metrics.</p><p>After starting the Spring Boot project and visiting the <code>/actuator/prometheus</code> path, you’ll see a large number of metrics, such as:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"># HELP executor_pool_size_threads The current number of threads in the pool</span><br><span class="line"># TYPE executor_pool_size_threads gauge</span><br><span class="line">executor_pool_size_threads&#123;application=&quot;ads-programad&quot;,name=&quot;asyncExecutor&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_servlet_request_seconds  </span><br><span class="line"># TYPE tomcat_servlet_request_seconds summary</span><br><span class="line">tomcat_servlet_request_seconds_count&#123;application=&quot;ads-programad&quot;,name=&quot;dispatcherServlet&quot;,&#125; 1.0</span><br><span class="line">tomcat_servlet_request_seconds_sum&#123;application=&quot;ads-programad&quot;,name=&quot;dispatcherServlet&quot;,&#125; 0.0</span><br><span class="line"># HELP executor_pool_core_threads The core number of threads for the pool</span><br><span class="line"># TYPE executor_pool_core_threads gauge</span><br><span class="line">executor_pool_core_threads&#123;application=&quot;ads-programad&quot;,name=&quot;asyncExecutor&quot;,&#125; 70.0</span><br><span class="line"># HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution</span><br><span class="line"># TYPE jvm_classes_unloaded_classes_total counter</span><br><span class="line">jvm_classes_unloaded_classes_total&#123;application=&quot;ads-programad&quot;,&#125; 0.0</span><br><span class="line"># HELP executor_completed_tasks_total The approximate total number of tasks that have completed execution</span><br><span class="line"># TYPE executor_completed_tasks_total counter</span><br><span class="line">executor_completed_tasks_total&#123;application=&quot;ads-programad&quot;,name=&quot;asyncExecutor&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_threads_config_max_threads  </span><br><span class="line"># TYPE tomcat_threads_config_max_threads gauge</span><br><span class="line">tomcat_threads_config_max_threads&#123;application=&quot;ads-programad&quot;,name=&quot;http-nio-9000&quot;,&#125; 500.0</span><br><span class="line"># HELP process_cpu_usage The &quot;recent cpu usage&quot; for the Java Virtual Machine process</span><br><span class="line"># TYPE process_cpu_usage gauge</span><br><span class="line">process_cpu_usage&#123;application=&quot;ads-programad&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_sessions_active_current_sessions  </span><br><span class="line"># TYPE tomcat_sessions_active_current_sessions gauge</span><br><span class="line">tomcat_sessions_active_current_sessions&#123;application=&quot;ads-programad&quot;,&#125; 0.0</span><br><span class="line"># HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use</span><br><span class="line"># TYPE jvm_memory_committed_bytes gauge</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;heap&quot;,id=&quot;G1 Eden Space&quot;,&#125; 3.5651584E7</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;heap&quot;,id=&quot;G1 Old Gen&quot;,&#125; 4.6137344E7</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;Compressed Class Space&quot;,&#125; 5767168.0</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;CodeHeap &#x27;non-profiled nmethods&#x27;&quot;,&#125; 8847360.0</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;CodeHeap &#x27;non-nmethods&#x27;&quot;,&#125; 2555904.0</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;Metaspace&quot;,&#125; 4.2287104E7</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;heap&quot;,id=&quot;G1 Survivor Space&quot;,&#125; 4194304.0</span><br><span class="line"># HELP tomcat_servlet_request_max_seconds  </span><br><span class="line"># TYPE tomcat_servlet_request_max_seconds gauge</span><br><span class="line">tomcat_servlet_request_max_seconds&#123;application=&quot;ads-programad&quot;,name=&quot;dispatcherServlet&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_connections_current_connections  </span><br><span class="line"># TYPE tomcat_connections_current_connections gauge</span><br><span class="line">tomcat_connections_current_connections&#123;application=&quot;ads-programad&quot;,name=&quot;http-nio-9000&quot;,&#125; 3.0</span><br><span class="line"># HELP tomcat_sessions_active_max_sessions  </span><br><span class="line"># TYPE tomcat_sessions_active_max_sessions gauge</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>In addition to the explicitly configured HTTP monitoring, there is also a large amount of basic monitoring information such as JVM metrics and machine load.</p><p>Beyond that, monitoring for other components is also easy to add, such as thread pools, HTTP connection pools, and custom metrics. You can refer to <a href="https://github.com/lcy362/springboot-prometheus-demo">https://github.com/lcy362/springboot-prometheus-demo</a></p><p>This way, regardless of how the Spring Boot project is deployed — whether using native Java deployment, Docker deployment, or deployment on Kubernetes — it’s very easy to obtain all the monitoring metrics data.</p><h2 id="Quick-Start-Steps"><a href="#Quick-Start-Steps" class="headerlink" title="Quick Start Steps"></a>Quick Start Steps</h2><h3 id="Step-1-Install-and-configure-Prometheus"><a href="#Step-1-Install-and-configure-Prometheus" class="headerlink" title="Step 1: Install and configure Prometheus"></a>Step 1: Install and configure Prometheus</h3><p>Download and install Prometheus on your server, then edit the <code>prometheus.yml</code> configuration file to set basic parameters such as the global scrape interval and evaluation interval. Once Prometheus is running, visit <code>http://localhost:9090</code> to access the Web UI.</p><h3 id="Step-2-Configure-data-collection-targets"><a href="#Step-2-Configure-data-collection-targets" class="headerlink" title="Step 2: Configure data collection targets"></a>Step 2: Configure data collection targets</h3><p>Define the monitoring targets in the <code>scrape_configs</code> section of the configuration file, including the <code>/actuator/prometheus</code> endpoint exposed by your application. Use Spring Actuator and Micrometer to expose Spring Boot metrics in Prometheus format, and Prometheus will automatically pull data at the configured interval.</p><h3 id="Step-3-Write-PromQL-queries"><a href="#Step-3-Write-PromQL-queries" class="headerlink" title="Step 3: Write PromQL queries"></a>Step 3: Write PromQL queries</h3><p>Use Prometheus’s built-in PromQL query language to flexibly query the collected time-series data. You can filter by metric name and labels, perform aggregations, calculate rates, and more to address various monitoring analysis needs.</p><h3 id="Step-4-Connect-Grafana-for-visualization"><a href="#Step-4-Connect-Grafana-for-visualization" class="headerlink" title="Step 4: Connect Grafana for visualization"></a>Step 4: Connect Grafana for visualization</h3><p>Add Prometheus as a data source in Grafana and use Grafana’s rich panels and dashboard features to visualize monitoring data in charts and other graphical formats. You can also configure alerting rules in Grafana to receive timely notifications when metrics become abnormal.</p><p>Original article: <a href="https://lichuanyang.top/posts/28288/">https://lichuanyang.top/posts/28288/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/28288/">https://lichuanyang.top/en/posts/28288/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/28288/</id>
    <link href="https://lichuanyang.top/en/posts/28288/"/>
    <published>2021-11-10T11:54:05.000Z</published>
    <summary>A Prometheus monitoring system tutorial that explains how its simple design supports complex functionality, starting from its design philosophy.</summary>
    <title>Prometheus Tutorial: Everything You Need to Know</title>
    <updated>2026-06-27T03:48:04.168Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="open-source-project" scheme="https://lichuanyang.top/en/tags/open-source-project/"/>
    <content>
      <![CDATA[<p>A high-performance Java IP address-to-country lookup tool. The core approach uses binary search over a pre-loaded array of IP ranges, achieving microsecond-level latency per query.</p><span id="more"></span><h2 id="Design-Thinking"><a href="#Design-Thinking" class="headerlink" title="Design Thinking"></a>Design Thinking</h2><p>An IP address is essentially a 32-bit integer. An “IP range” is simply a continuous range of integers. Given an IP, we need to find which range it falls into — a classic <strong>interval search</strong> problem.</p><p>The most direct approach is to iterate through all IP ranges — O(n) complexity. But there are hundreds of thousands of IP ranges globally, so iteration is too slow. A better approach is to sort the starting addresses of the IP ranges into an array and use <strong>binary search</strong> — O(log n) complexity, with only about 19 comparisons needed for hundreds of thousands of ranges.</p><h2 id="Data-Source"><a href="#Data-Source" class="headerlink" title="Data Source"></a>Data Source</h2><p>IP address database data can be obtained for free from <a href="http://download.ip2location.com/lite/">IP2Location Lite</a>. Sample data format:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">16781312,JP</span><br><span class="line">16785408,CN</span><br><span class="line">16793600,JP</span><br></pre></td></tr></table></figure><p>Each line contains two fields: the IP range’s <strong>starting address</strong> (converted to an integer) and the <strong>country code</strong>.</p><p>The original data also includes the end address of each IP range, but IP2Location’s data segments are <strong>continuous and gap-free</strong> — the end address of one range is exactly the starting address of the next range minus one. Therefore, we can store only the starting addresses, with the end address naturally determined by the next range’s starting address. This optimization saves half the memory.</p><h2 id="Data-Structure"><a href="#Data-Structure" class="headerlink" title="Data Structure"></a>Data Structure</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">IpCountryLookup</span> &#123;</span><br><span class="line">    <span class="comment">// IP range starting addresses (sorted)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span>[] startIps;</span><br><span class="line">    <span class="comment">// Corresponding country codes</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String[] countryCodes;</span><br><span class="line">    <span class="comment">// Total number of ranges</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> size;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Two parallel arrays: <code>startIps[i]</code> and <code>countryCodes[i]</code> correspond one-to-one. The binary search is performed on <code>startIps</code>, and once found, the result is taken from <code>countryCodes</code>.</p><h2 id="Core-Implementation-Binary-Search"><a href="#Core-Implementation-Binary-Search" class="headerlink" title="Core Implementation: Binary Search"></a>Core Implementation: Binary Search</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> String <span class="title function_">lookup</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">    <span class="type">long</span> <span class="variable">ipLong</span> <span class="operator">=</span> ipToLong(ip);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Binary search: find the last starting address &lt;= ipLong</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">left</span> <span class="operator">=</span> <span class="number">0</span>, right = size - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (left &lt;= right) &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">mid</span> <span class="operator">=</span> left + (right - left) / <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">if</span> (startIps[mid] &lt;= ipLong) &#123;</span><br><span class="line">            left = mid + <span class="number">1</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            right = mid - <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// &#x27;right&#x27; is the position of the last element &lt;= ipLong</span></span><br><span class="line">    <span class="keyword">if</span> (right &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> countryCodes[right];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;Unknown&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Convert IP string to long</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">long</span> <span class="title function_">ipToLong</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">    String[] parts = ip.split(<span class="string">&quot;\\.&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> (Long.parseLong(parts[<span class="number">0</span>]) &lt;&lt; <span class="number">24</span>)</span><br><span class="line">         + (Long.parseLong(parts[<span class="number">1</span>]) &lt;&lt; <span class="number">16</span>)</span><br><span class="line">         + (Long.parseLong(parts[<span class="number">2</span>]) &lt;&lt; <span class="number">8</span>)</span><br><span class="line">         + Long.parseLong(parts[<span class="number">3</span>]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Performance-Data"><a href="#Performance-Data" class="headerlink" title="Performance Data"></a>Performance Data</h2><ul><li><strong>Memory Usage</strong>: ~300,000 IP ranges, two arrays with ~300K elements each, total about 4 MB</li><li><strong>Query Latency</strong>: &lt; 10 microseconds per query (~19 comparisons for binary search)</li><li><strong>QPS</strong>: Easily handles millions of queries per second on a single machine</li><li><strong>Data Updates</strong>: IP database updates roughly once a month; a scheduled task automatically pulls and reloads</li></ul><h2 id="Other-Data-Source-Comparisons"><a href="#Other-Data-Source-Comparisons" class="headerlink" title="Other Data Source Comparisons"></a>Other Data Source Comparisons</h2><table><thead><tr><th>Data Source</th><th>Granularity</th><th>Free Version</th><th>Characteristics</th></tr></thead><tbody><tr><td>IP2Location Lite</td><td>Country</td><td>Yes</td><td>Clean data, uniform format</td></tr><tr><td>GeoIP2 (MaxMind)</td><td>City</td><td>Yes (limited)</td><td>High community recognition, good Java API</td></tr><tr><td>Chunzhen IP Database</td><td>ISP</td><td>Yes</td><td>Best accuracy within China</td></tr><tr><td>ipip.net</td><td>City&#x2F;ISP</td><td>No</td><td>Most accurate for China, but paid</td></tr></tbody></table><p>If you only need country-level granularity, IP2Location Lite is sufficient. For city-level accuracy, MaxMind GeoLite2 is recommended.</p><h2 id="Deployment"><a href="#Deployment" class="headerlink" title="Deployment"></a>Deployment</h2><p>The project is built on Spring Boot and supports two deployment methods:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Run directly</span></span><br><span class="line">mvn spring-boot:run</span><br><span class="line"></span><br><span class="line"><span class="comment"># Docker</span></span><br><span class="line">docker run -p 8080:8080 lcy362/ip-country</span><br></pre></td></tr></table></figure><p>Try it online: <a href="http://ip-country.lichuanyang.top/">http://ip-country.lichuanyang.top/</a></p><p>Project source: <a href="https://github.com/lcy362/ip-country">https://github.com/lcy362/ip-country</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/36780/">https://lichuanyang.top/en/posts/36780/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/36780/</id>
    <link href="https://lichuanyang.top/en/posts/36780/"/>
    <published>2021-10-12T11:25:15.000Z</published>
    <summary>A high-performance IP address country lookup tool implemented in Java, based on IP2Location data source with scheduled updates.</summary>
    <title>Building a Simple High-Performance Java IP Address Country Lookup Tool</title>
    <updated>2026-06-27T03:51:55.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="iterm2" scheme="https://lichuanyang.top/en/tags/iterm2/"/>
    <category term="ssh-auto-login" scheme="https://lichuanyang.top/en/tags/ssh-auto-login/"/>
    <category term="ssh-passwordless-login" scheme="https://lichuanyang.top/en/tags/ssh-passwordless-login/"/>
    <content>
      <![CDATA[<h2 id="Pain-Point-Frequent-SSH-Logins"><a href="#Pain-Point-Frequent-SSH-Logins" class="headerlink" title="Pain Point: Frequent SSH Logins"></a>Pain Point: Frequent SSH Logins</h2><p>If you’re like me and frequently need to access different remote servers, recording server IPs and entering passwords can be very tedious. Fortunately, by making some configurations in iTerm2, this pain point can be well addressed. The final result is similar to configuring SSH bookmarks, enabling iTerm2 to remember SSH passwords and achieve passwordless and automatic login.</p><span id="more"></span><h2 id="Complete-Configuration-Steps"><a href="#Complete-Configuration-Steps" class="headerlink" title="Complete Configuration Steps"></a>Complete Configuration Steps</h2><p>iTerm2 (<a href="https://iterm2.com/">https://iterm2.com/</a>) is a widely used terminal alternative on Mac, providing many powerful features. To achieve SSH bookmarks with passwordless and automatic login, the key lies in three features: profile, trigger, and password manager.</p><p>Profile, as the name suggests, is a set of configurations. When we normally open iTerm2, we’re actually opening the default profile. The entry point for configuring profiles is under the Profiles option in the toolbar, where you can add or edit existing profiles. Change the “Command” section under the General tab of the desired profile to “Command”, and fill in the SSH command, such as <code>ssh root@1.1.1.1</code>, so that the SSH command is automatically executed when the profile is opened. Other text, color, and other configurations in the profile are not critical and can be filled in as needed.</p><p>Trigger is also a feature of the profile. The entry point is under the Advanced tab in the profile configuration page. Its purpose is to use a keyword to trigger an action. What we need to do now is use the keyword “password” to trigger opening the password manager. The operation is simple: add a trigger, fill in “password” for the Regular Expression, select “Open Password Manager” for the Action, and make sure to check both the “Instant” and “Enabled” options.</p><p>The last thing to configure is the password manager. The password manager is a password management tool that comes as a default plugin in iTerm2. Its entry point is under the Window tab in the toolbar. Open the password manager and enter the passwords you want to save.</p><h2 id="Security-Reminder"><a href="#Security-Reminder" class="headerlink" title="Security Reminder"></a>Security Reminder</h2><p>This way, we’ve implemented “bookmarks” in iTerm2 to save remote server addresses and passwords. When using them, simply access the corresponding profile, wait for the password manager to pop up, select the corresponding password record, and click to input it.</p><p>Source: <a href="https://lichuanyang.top/en/posts/20763/">https://lichuanyang.top/en/posts/20763/</a></p><hr><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><h3 id="Step-1-Generate-SSH-Key"><a href="#Step-1-Generate-SSH-Key" class="headerlink" title="Step 1: Generate SSH Key"></a>Step 1: Generate SSH Key</h3><p>Run <code>ssh-keygen</code> in the terminal to generate an SSH key pair. Add the public key to the target server’s <code>~/.ssh/authorized_keys</code> to ensure the passwordless login foundation is set up.</p><h3 id="Step-2-Configure-iTerm2-Profile"><a href="#Step-2-Configure-iTerm2-Profile" class="headerlink" title="Step 2: Configure iTerm2 Profile"></a>Step 2: Configure iTerm2 Profile</h3><p>Open iTerm2, go to Profiles → Open Profiles → Edit Profiles. In the General tab, set the Command to an SSH command in the format <code>ssh root@1.1.1.1</code>.</p><h3 id="Step-3-Add-Bookmarks"><a href="#Step-3-Add-Bookmarks" class="headerlink" title="Step 3: Add Bookmarks"></a>Step 3: Add Bookmarks</h3><p>In the Profile editor’s Advanced tab, add a Trigger: enter <code>password</code> for Regular Expression, select Open Password Manager for Action, and check Instant and Enabled. Then go to Window → Password Manager to store server passwords.</p><h3 id="Step-4-Verify-Auto-Login"><a href="#Step-4-Verify-Auto-Login" class="headerlink" title="Step 4: Verify Auto Login"></a>Step 4: Verify Auto Login</h3><p>Select the configured Profile from the Profiles menu, wait for the Password Manager to pop up, select the corresponding password record, and confirm that SSH login completes automatically.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/20763/</id>
    <link href="https://lichuanyang.top/en/posts/20763/"/>
    <published>2021-10-08T08:45:42.000Z</published>
    <summary>Configure SSH bookmarks in iTerm2 to save passwords and enable auto login, eliminating the pain of repeatedly entering IP addresses and passwords.</summary>
    <title>Configuring SSH Bookmarks in iTerm2 for Password Storage and Auto Login</title>
    <updated>2026-06-27T02:20:10.608Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="tech-talks" scheme="https://lichuanyang.top/en/tags/tech-talks/"/>
    <category term="tech-talk-topics" scheme="https://lichuanyang.top/en/tags/tech-talk-topics/"/>
    <content>
      <![CDATA[<p>Tech talks exist in nearly every company. The effectiveness varies from person to person and team to team. In some teams, tech talks gradually become a useless time killer due to various reasons.</p><p>This article aims to improve the baseline of tech talks through some fundamental rules, starting from topic selection and other aspects, striving to help 80% of people deliver a tech talk that scores above 80 points.</p><span id="more"></span><h2 id="Topic-Selection"><a href="#Topic-Selection" class="headerlink" title="Topic Selection"></a>Topic Selection</h2><h3 id="Basic-Principles"><a href="#Basic-Principles" class="headerlink" title="Basic Principles"></a><strong>Basic Principles</strong></h3><p>First, you need to understand that in-person tech talks are not the only way to share knowledge. Other forms include writing blog posts and articles, making videos, or simply sharing a sentence or two — all are common sharing formats. For different types of knowledge, you should consider which format is more appropriate.</p><p>For example, for an introductory introduction to a new technology, writing an article is recommended. Readers can quickly scan or search for key information, which is much more efficient than sitting down to listen to a presentation.</p><h3 id="Recommended-Topics"><a href="#Recommended-Topics" class="headerlink" title="Recommended Topics"></a><strong>Recommended Topics</strong></h3><p>Generally speaking, good topics should have strong practical value, be able to attract the audience, and have an appropriate level of difficulty. They should strike a balance between being understandable and providing new insights for the audience.</p><p>Here are some examples of good topics and some things to keep in mind:</p><ul><li>For introducing new technologies, you need to clearly explain the background of why the new technology was created, what problems it solves, its advantages and disadvantages compared to previous similar solutions, and what new problems it introduces, etc.</li><li>For explaining obscure or difficult technologies, the topic scope can be smaller. You need solid presentation skills based on a thorough understanding of the problem to ensure everyone can follow along.</li><li>In-depth technical discussions on specific details are similar to the previous category — pay attention to background introduction.</li><li>For high-level summaries of a category of problems, such as cache consistency, distributed IDs, distributed transactions, etc., these topics can start from real existing problems, analyze the pros and cons of different solutions, and discuss the evolution of technical approaches.</li><li>Business-oriented talks are best when choosing common business domains, and pay attention to background introduction.</li></ul><h2 id="Content-Preparation"><a href="#Content-Preparation" class="headerlink" title="Content Preparation"></a>Content Preparation</h2><p>For talk effectiveness, while it will be influenced by various factors, the most important thing at its core is whether the content has enough substance.</p><p>To have substance, the best approach is consistent daily accumulation. I recommend thinking more in your daily work and building deeper understanding of the things you use. I recommend reading the book “How to Take Smart Notes” — you need continuous input in daily life to produce output.</p><p>Additionally:</p><p>During preparation, think and summarize to form your own viewpoints. Simply rehashing others’ material actually wastes your own time.</p><p>Try to incorporate real application scenarios into your talk content.</p><p>For difficult knowledge points, pay attention to presentation skills to make sure the audience can understand. Consider doing a dry run with one or two colleagues first on the key parts of your talk.</p><p>Watching videos can help you not only learn the content itself but also see how others present, which is quite helpful for improving your talk’s effectiveness.</p><p>Beyond the actual content, I recommend preparing the following:</p><p>Pre-study materials. You can prepare materials in advance that help understand the talk content, such as certain chapters from a book or specific articles, and ask everyone to read them beforehand. The speaker needs to provide estimated reading time for each material — the total reading time for a single talk should not exceed one hour.</p><p>Detailed explanation of background, especially when business knowledge or uncommon scenarios are involved.</p><h2 id="Talk-Process"><a href="#Talk-Process" class="headerlink" title="Talk Process"></a>Talk Process</h2><p>After preparing the talk content, you need to find someone to review it. The reviewer can be your mentor, leader, or someone else very familiar with the relevant field.</p><p>The reviewer, based on a thorough understanding of the talk content, mainly reviews the topic selection and content to ensure they meet the requirements listed in this article. They should also try to ensure the content doesn’t contain technical errors.</p><p>The talk materials must be sent out in advance to give the audience a chance to familiarize themselves with them.</p><p>During the talk, pay attention to pacing — don’t speak too fast.</p><p>Original article: <a href="https://lichuanyang.top/posts/54216/">https://lichuanyang.top/posts/54216/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/54216/">https://lichuanyang.top/en/posts/54216/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/54216/</id>
    <link href="https://lichuanyang.top/en/posts/54216/"/>
    <published>2021-08-31T06:27:43.000Z</published>
    <summary>A systematic guide on topic selection, structure, and presentation skills to deliver a tech talk that scores above 80 points.</summary>
    <title>How to Give a Good Tech Talk</title>
    <updated>2026-06-08T07:50:49.099Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Cloud Native" scheme="https://lichuanyang.top/en/categories/Cloud-Native/"/>
    <category term="microservices" scheme="https://lichuanyang.top/en/tags/microservices/"/>
    <category term="cloud-native-architecture" scheme="https://lichuanyang.top/en/tags/cloud-native-architecture/"/>
    <content>
      <![CDATA[<h2 id="Definition-of-Cloud-Native"><a href="#Definition-of-Cloud-Native" class="headerlink" title="Definition of Cloud Native"></a>Definition of Cloud Native</h2><p>In recent years, cloud native has become an increasingly popular concept across the open-source community. But what exactly is cloud native? Is it an architecture? A platform? What does it affect? System security? Development efficiency? So today, let’s dig deep and sort out what cloud native really is.</p><span id="more"></span><p>To understand what cloud native is, we need to start with its name. The English name for cloud native is “cloud native,” which obviously contains two parts: cloud and native. Cloud means the application runs in the cloud, not locally. Native means the application should run in the way best suited for the cloud, not just be migrated from local to the cloud.</p><p>So what kind of application is suited for the cloud? It’s one that maximizes the use of cloud capabilities and leverages the advantages of the cloud.</p><p>The core advantage of cloud computing is essentially just centralizing more resources for unified management and allocation, making it more convenient to flexibly allocate resources on demand and improve resource utilization.</p><p>By analogy, many people have used streaming frameworks like Storm. What are their advantages? One important factor is the ability to break down a complex workflow into multiple sub-nodes, where each node can be configured with different levels of concurrency based on its needs. Nodes with higher concurrency demands can receive more resources. This way, resource utilization is improved.</p><h2 id="Microservices"><a href="#Microservices" class="headerlink" title="Microservices"></a>Microservices</h2><p>For microservices, it’s similar — splitting different functions into separate services allows independent scaling at a finer granularity.</p><p>It’s worth noting that splitting includes not only separating different business domains, but also separating business code, third-party software (third-party libraries), and non-functional features (high availability, security, observability, etc.) into three distinct categories.</p><p>Pure business splitting has actually been happening since very early stages of software development. The trend accompanying the rise of cloud native is to maximize the separation of non-business code portions from cloud applications, allowing cloud infrastructure to take over the many non-functional features originally in applications (such as elasticity, resilience, security, observability, canary releases, etc.) — this is the so-called service mesh.</p><p>Since resources and applications in the cloud are not strongly bound, to make resource utilization more convenient, we need a more universal runtime format that allows applications to have a certain degree of decoupling from their runtime environment. </p><h2 id="Containerization"><a href="#Containerization" class="headerlink" title="Containerization"></a>Containerization</h2><p>This is container technology. Containers provide a logical packaging mechanism. Applications packaged with this mechanism can operate independently of their actual runtime environment. Using this decoupling, regardless of whether the target environment is a private data center, public cloud, or a developer’s personal laptop, you can easily and consistently deploy container-based applications. Containerization makes the concerns of developers and IT operations teams distinct — developers focus on application logic and dependencies, while IT operations teams can focus on deployment and management without being distracted by specific software versions and application-specific configurations.</p><h2 id="Observability"><a href="#Observability" class="headerlink" title="Observability"></a>Observability</h2><p>On the other hand, after splitting services into finer granularity, the system’s inherent complexity obviously increases. For example, local calls become network requests, and call chains cannot be reflected through code structure. Therefore, operations need to be more intelligent and automated to ensure stronger stability of individual services. At the same time, a powerful monitoring system is needed that can analyze dependencies between microservices and quickly detect anomalies in the system.</p><p>Furthermore, with smaller individual services and comprehensive monitoring data, we can deploy more frequently, even deploying directly to production after each change. If a deployment has issues, we can detect them promptly through monitoring, thereby controlling losses to a minimum. Small-scale deployments also make it easier to locate problems or roll back.</p><p>From the analysis above, we can organize some keywords related to cloud native, such as service-oriented, elastic, observable, resilient, automated, etc. These keywords can be summarized into four categories: microservices, DevOps, continuous delivery, and containerization.</p><p>The key characteristics of these four categories are:</p><p>Microservices: Can be independently deployed, updated, restarted, and scaled</p><p>DevOps: Automated, rapid, development-operations collaboration</p><p>Continuous delivery: Frequent releases, fast feedback</p><p>Containerization: Logical packaging mechanism</p><h2 id="Cloud-Native-Mindset"><a href="#Cloud-Native-Mindset" class="headerlink" title="Cloud Native Mindset"></a>Cloud Native Mindset</h2><p>We’ve covered a lot of theory above. So what are the concrete implementation paths for adopting cloud native? We can consider the following aspects:</p><ol><li>Business service splitting: This is a very fundamental thing in software development. Splitting needs to follow basic design principles like SOLID.</li><li>Comprehensive monitoring system: Including collecting information across log, trace, metric, and alert dimensions. Logs focus on recording information during code execution, traces are mainly for tracking the flow of the same request across different services, metrics are for monitoring system runtime status, and alerts are for anomaly notifications. The industry already has many open-source implementations, such as Prometheus, Jaeger, etc.</li><li>Containers and container orchestration: This basically means Docker and K8s.</li><li>Middleware mesh化: Business applications only retain a thin client layer, with the main logic in the middleware placed at the mesh layer.</li><li>DevOps and continuous delivery: This mainly involves development workflows and many process-related aspects of development-operations collaboration. In a cloud environment, we advocate a pattern of small batches, frequent releases, and fast feedback.</li></ol><p>I am Liu Sha. I hope this article can help everyone better understand what exactly cloud native is. Actually, cloud native is simple to describe — it’s about adopting various approaches to better utilize cloud resources. But when explained in detail, it’s a very comprehensive system covering everything from development to operations. Welcome to follow my WeChat public account (Mobility), or visit my <a href="https://lichuanyang.top/">personal website</a>. I will gradually expand on all aspects of cloud native in future articles.</p><h2 id="FAQ"><a href="#FAQ" class="headerlink" title="FAQ"></a>FAQ</h2><h3 id="Q-Is-cloud-native-just-about-adopting-Kubernetes"><a href="#Q-Is-cloud-native-just-about-adopting-Kubernetes" class="headerlink" title="Q: Is cloud native just about adopting Kubernetes?"></a>Q: Is cloud native just about adopting Kubernetes?</h3><p>No. Kubernetes is the core container orchestration tool in the cloud native ecosystem, but cloud native goes far beyond K8s. Cloud native is a comprehensive methodology covering microservices architecture, containerization, DevOps, continuous delivery, observability, and more. Running on K8s doesn’t automatically mean you’re “cloud native” — if your application is still a monolithic blob, with no automated CI&#x2F;CD, no monitoring or alerting, then you’re simply “running a traditional application on K8s.”</p><h3 id="Q-Is-it-necessary-to-migrate-legacy-applications-to-cloud-native"><a href="#Q-Is-it-necessary-to-migrate-legacy-applications-to-cloud-native" class="headerlink" title="Q: Is it necessary to migrate legacy applications to cloud native?"></a>Q: Is it necessary to migrate legacy applications to cloud native?</h3><p>It depends on the context. If the application is small in scale, has a low iteration frequency, and a small team, forcing a full cloud native adoption will only add complexity. But if the application needs frequent iteration, elastic scaling, high availability, or the team is large enough to warrant microservice decomposition — that’s when cloud native delivers value. In short: don’t adopt cloud native for its own sake; adopt it to solve real problems.</p><h3 id="Q-What’s-the-difference-between-containerization-and-virtualization"><a href="#Q-What’s-the-difference-between-containerization-and-virtualization" class="headerlink" title="Q: What’s the difference between containerization and virtualization?"></a>Q: What’s the difference between containerization and virtualization?</h3><p>Virtual machines (VMs) virtualize at the hardware layer — each VM has its own OS kernel, with slow startup and significant resource overhead. Containers virtualize at the OS layer — all containers share the host kernel, isolating only the application and its dependencies, with fast startup (seconds) and minimal resource footprint. A simple analogy: VMs are like “each household has its own kitchen in an apartment building,” while containers are like “sharing one large kitchen, each person having their own stove.”</p><h3 id="Q-What’s-the-relationship-between-microservices-and-cloud-native"><a href="#Q-What’s-the-relationship-between-microservices-and-cloud-native" class="headerlink" title="Q: What’s the relationship between microservices and cloud native?"></a>Q: What’s the relationship between microservices and cloud native?</h3><p>Microservices are a core component of cloud native architecture, but not the whole picture. Microservices address “how to split applications for independent deployment and scaling,” while cloud native also encompasses “how these microservices run” (containers and orchestration), “how they are delivered” (CI&#x2F;CD), “how they are monitored” (observability), “how they communicate” (Service Mesh), and more. Think of microservices as the “business layer” of cloud native architecture.</p><p>Original article: <a href="https://lichuanyang.top/posts/42843/">https://lichuanyang.top/posts/42843/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/42843/</id>
    <link href="https://lichuanyang.top/en/posts/42843/"/>
    <published>2021-06-09T11:54:37.000Z</published>
    <summary>Starting from the literal meaning of Cloud Native, systematically organizing the definition, core concepts, and technology ecosystem of cloud native.</summary>
    <title>What Exactly is Cloud Native?</title>
    <updated>2026-06-27T03:50:56.855Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Miscellany" scheme="https://lichuanyang.top/en/categories/Tech-Miscellany/"/>
    <category term="personal-growth" scheme="https://lichuanyang.top/en/tags/personal-growth/"/>
    <category term="book-notes" scheme="https://lichuanyang.top/en/tags/book-notes/"/>
    <content>
      <![CDATA[<p>“Amoeba Management” is a classic book about work attitudes and work methods. In today’s world of severe labor-capital conflicts and increasingly rigid class structures, it’s difficult to fully practice the philosophy of the book’s author, Kazuo Inamori. But this doesn’t mean the book has no value. Working hard and enhancing one’s own value has always been a spiritual pursuit hidden deep within the human heart. What we need to do is ensure that the returns from value enhancement go to ourselves.</p><span id="more"></span><p>Before reading this book, I strongly recommend carefully thinking about what your relationship with work actually is. If you’re the company’s owner, there’s nothing more to say — the company’s returns are yours. If you’re not, then you need to figure out where the value you create goes: does it result in financial returns, capability improvement, credentials that can serve as proof of your abilities in the future, or does it just help your boss buy two more sports cars?</p><p>I previously read an article that suggested treating yourself as a company to manage, and treating your employer as your client. I think this is an excellent way of thinking. You need to consider whether your “company” should pursue cash income immediately, or first hone your capabilities and reputation in anticipation of greater returns in the future. You also need to consider whether the transaction with your employer is fair.</p><p>Once you’ve thought through these things, then come read “Amoeba Management.” Otherwise, you might think this book was written by Teacher Ma. Speaking of which, Teacher Ma did practice many of the book’s concepts when he was an English teacher. Without the hard work during those “spare” hours back then, where would today’s blessings come from?</p><p>Alright, let’s get to the main topic. I’ll discuss the work-related values conveyed in the book from three aspects: the meaning of work, how to approach work, and work methods and attitudes.</p><h2 id="The-Meaning-of-Work"><a href="#The-Meaning-of-Work" class="headerlink" title="The Meaning of Work"></a>The Meaning of Work</h2><p>As I said at the beginning of this article, working hard and creating value is actually a very important spiritual pursuit hidden deep within the human heart. We can see that many people who have achieved financial freedom still seek work opportunities — either starting businesses or finding comfortable and free work. This is the deep-seated spiritual drive that motivates human behavior.</p><p>Beyond the financial returns obtained through work,</p><blockquote><p>Working wholeheartedly and striving for excellence is itself a practice of character cultivation that promotes our growth. Labor can bring a sense of joy, help people understand the meaning of life, and labor is a noble act.</p></blockquote><p>If you can accept the premise of “treating yourself as a company to manage,” you’ll find that immediate financial returns are actually not that important. Personal growth and the happiness brought by work are more meaningful rewards, and after achieving these, financial returns will naturally follow.</p><h2 id="How-to-Approach-Work"><a href="#How-to-Approach-Work" class="headerlink" title="How to Approach Work"></a>How to Approach Work</h2><blockquote><p>Most beginners have to start with work they don’t like.</p></blockquote><p>In this world, the number of people who truly turn their hobbies into work is inherently small. Among that small group, the vast majority will feel after working that it’s no longer their hobby. Because the difference between something as a hobby and as a work is enormous. Many people love gaming, and many envy professional esports players, thinking they’re doing what they love for a living. But if you pay attention to the esports scene, you’ll find plenty of people who don’t train properly, and some take every opportunity to play other games. They originally became professional players because they loved the game, only to find themselves gradually losing interest.</p><p>So, doing work you don’t like is the norm in this world. What we need to do is learn to love the work itself. The author provides some methods for how to make yourself love your work.</p><blockquote><p>Only through effort can you come to love your work.<br>Delight in small successes at work, use the energy they bring as motivation, and work even harder.</p></blockquote><h2 id="Work-Methods-and-Attitudes"><a href="#Work-Methods-and-Attitudes" class="headerlink" title="Work Methods and Attitudes"></a>Work Methods and Attitudes</h2><p>First, use high goals as motivation.</p><blockquote><p>Use “future tense” when assessing capabilities</p></blockquote><p>Imagine this: if you placed yourself from some time ago into the present moment, wouldn’t you find what you’re doing now difficult? This is a normal phenomenon. When you’re on a normal development path, capability improvement happens不知不觉.</p><p>With high goals, you need to continuously put in effort and think about how to achieve the goals.</p><blockquote><p>You must think. Without serious thought, nothing can be achieved.</p></blockquote><p>Only you yourself can be responsible for your own growth — relying on anyone else is unrealistic. Recently, I’ve been reading complaints on Maimai, and many people’s dissatisfaction with their companies is “no growth.” This confuses me — what exactly is the relationship between growth and a company? This goes back to the relationship between people and companies. A company is your client — you complete tasks and receive compensation. What does this have to do with personal growth? Growth requires your own reflection, not waiting for someone to assign it to you, saying “do these things and you’ll grow.” The only scenario I can think of where someone at a company gains nothing is if they already understand every aspect of the company and could easily start another company to defeat it.</p><p>Additionally, strive for perfectionism.</p><blockquote><p>Erasers can never completely erase mistakes</p></blockquote><p>Many mistakes, even if you compensate for them afterward, will inevitably leave some traces. Whatever you do, try your best to make it perfect — at least within the scope of your own understanding. For example, when developing a feature, think through the entire process to ensure there are no logical flaws or implementation bugs, rather than just looking at the product spec once and implementing it simply, leaving everything else to the testers.</p><blockquote><p>Work results &#x3D; Thinking × Passion × Ability</p></blockquote><p>At the end of the book, the author provides a formula. For work, thinking and passion are just as important as ability. And given this understanding, improving passion and changing your thinking is far easier than improving ability. So, wanting to improve work effectiveness isn’t actually that difficult — the key is to build a reasonable understanding of work.</p><p>I am Liusha, thank you for reading this article. If you found it helpful, please give it a like and follow. If you have questions, you can also reach me on my <a href="https://lichuanyang.top/">personal blog</a> or WeChat public account (Mobility).</p><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-Should-programmers-read-a-book-like-this"><a href="#Q-Should-programmers-read-a-book-like-this" class="headerlink" title="Q: Should programmers read a book like this?"></a>Q: Should programmers read a book like this?</h3><p>Yes, but I recommend approaching it with a critical mindset rather than accepting everything at face value. The book’s core value lies in getting you to first think through the question: “What is my relationship with work?” — are you working for the boss, or are you building skills and reputation for yourself? Once you’ve sorted that out, the book’s ideas about passion, perfectionism, and sustained effort become genuinely informative. I suggest pairing it with the mental framework of “treating yourself as a company” for the best results.</p><h3 id="Q-What-is-work-for"><a href="#Q-What-is-work-for" class="headerlink" title="Q: What is work for?"></a>Q: What is work for?</h3><p>It’s both an economic source and a spiritual pursuit. The financial side is obvious, but more importantly, the drive to create value is deeply embedded in human nature — many financially independent people still choose to work. The book’s answer is direct: work is not just a means of earning compensation, but a practice of character cultivation, a source of joy, and a way to understand the meaning of life. If you can’t fully embrace this view yet, start with “run yourself as a company” — at least make sure what you produce at work translates concretely into your own accumulation.</p><h3 id="Q-How-do-you-learn-to-enjoy-work-you-don’t-like"><a href="#Q-How-do-you-learn-to-enjoy-work-you-don’t-like" class="headerlink" title="Q: How do you learn to enjoy work you don’t like?"></a>Q: How do you learn to enjoy work you don’t like?</h3><p>Doing work you don’t like is the norm — very few people truly turn a hobby into a career, and even those who do often find the hobby losing its charm once it becomes work. The book offers two practical approaches: first, <strong>put in effort first</strong> — the positive feedback from your effort gradually builds engagement; second, <strong>rejoice in small successes at work</strong> — let every “I figured it out” moment fuel the energy to keep going. This isn’t about motivational platitudes — it’s about building a positive feedback loop through action.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/3513/</id>
    <link href="https://lichuanyang.top/en/posts/3513/"/>
    <published>2021-05-15T09:29:34.000Z</published>
    <summary>Book notes on Kazuo Inamori's 'Amoeba Management', reflecting on work attitudes and methods, exploring how to achieve personal value enhancement through work.</summary>
    <title>Book Notes: Kazuo Inamori's 'Amoeba Management' — Reflecting on How We Should Work</title>
    <updated>2026-06-27T02:41:13.961Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="development-practices" scheme="https://lichuanyang.top/en/tags/development-practices/"/>
    <content>
      <![CDATA[<h2 id="Why-Code-Review-is-Needed"><a href="#Why-Code-Review-is-Needed" class="headerlink" title="Why Code Review is Needed"></a>Why Code Review is Needed</h2><p>In a typical development workflow, there are various review stages — product spec reviews, design reviews, test case reviews, code reviews, and so on. To conduct these reviews, meetings are inevitably introduced. Some people might think these activities are useless and a waste of time. In reality, they are extremely important. If you have the impression that reviews don’t matter, it’s likely due to one of two reasons: either a lack of understanding, or poor review practices in your environment.</p><span id="more"></span><h2 id="Value-of-Review-for-the-Team"><a href="#Value-of-Review-for-the-Team" class="headerlink" title="Value of Review for the Team"></a>Value of Review for the Team</h2><p>Regarding the value of reviews, the most significant impact for a team is raising the team’s baseline. Imagine if no reviews were conducted at any stage — a product spec is handed off, developers just start coding, and any individual oversight could bring endless risks to the entire project. Multiple review stages essentially describe a business solution in different forms — product specs, code, test cases — and then involve more people to inspect each form, preventing individual oversights from creating business risks.</p><p>Furthermore, the focus of this article is to discuss the value of participating in reviews for an individual, beyond the benefits it brings to the team.</p><h2 id="Value-of-Review-for-the-Reviewee"><a href="#Value-of-Review-for-the-Reviewee" class="headerlink" title="Value of Review for the Reviewee"></a>Value of Review for the Reviewee</h2><p>First, for the person being reviewed — the author of the product spec, code, or other deliverables — a review is essentially a process where others help you improve. It’s a rare opportunity to receive direct feedback. For many people, compared to their student days, the biggest challenge of learning during their career is that no one “grades” their homework. After reading books or watching videos, there’s no way to confirm whether they’ve truly understood the material. Timely feedback, regardless of what you’re learning, is crucial. Among the reviewers will be your superiors and more capable colleagues. If they can thoroughly understand your solution or code, they can certainly provide valuable feedback. A review essentially gives you the opportunity to receive their feedback.</p><p>This implicitly requires the submitter to express their solution clearly, so others can participate in the review with minimal effort. For example, when writing code, use appropriate design patterns and write readable code. When writing documents, have a well-organized layout with proper sections and highlight key points. When presenting a solution to others, consider the audience’s knowledge background to ensure they can understand your proposal.</p><p>Another important aspect of reviews for the submitter is risk and responsibility sharing. Regardless of your company’s policies or your position, when a project you’re responsible for doesn’t go well, a significant portion of the consequences will ultimately fall on you. This may come as direct performance penalties, or as others questioning your capabilities — either way, it’s something nobody wants to face. Reviews transfer some of these consequences to the entire team. Of course, don’t think too much — the primary responsibility still remains with the submitter. This again highlights the importance of clear expression. The submitter needs to ensure that the key points of their solution are effectively communicated to the reviewers. If the reviewers successfully absorb this information, have thorough discussions during the review, and reach a consensus, that part of the decision can be considered as jointly made by the team.</p><h2 id="Value-of-Review-for-the-Reviewer"><a href="#Value-of-Review-for-the-Reviewer" class="headerlink" title="Value of Review for the Reviewer"></a>Value of Review for the Reviewer</h2><p>Now, let’s look at the value for reviewers — what does actively participating in others’ reviews offer? The primary benefit is a learning opportunity. As the saying goes, “When three people walk together, there must be one who can teach me.” From others’ solutions, you can certainly discover excellent points worth learning. When you find that someone else’s approach differs from your own expectations, it’s an opportunity for deep thinking — you can continuously question yourself to determine which approach is better until you reach an irrefutable conclusion. This kind of deep thinking significantly improves your own thinking methods.</p><p>Additionally, participating in reviews is also a way to rapidly gain experience. Reviewing is essentially spending review time to achieve an effect nearly identical to doing the work yourself. If you review thoroughly enough, you’ll gain a comprehensive understanding of how something is done, and this experience can fully count as part of your project experience.</p><h2 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h2><p>This article comes to an end here. I hope you’ve gained an understanding of why reviews should be done and how to conduct them. Feel free to reach out to me through my WeChat official account (Mobility) or <a href="https://lichuanyang.top/">personal website</a>.</p><p>Source: <a href="https://lichuanyang.top/en/posts/57205/">https://lichuanyang.top/en/posts/57205/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/57205/</id>
    <link href="https://lichuanyang.top/en/posts/57205/"/>
    <published>2021-04-27T10:07:11.000Z</published>
    <summary>Analyzing the value of code review from the perspective of personal growth — not just about finding bugs, but a key means of team knowledge sharing and code quality improvement.</summary>
    <title>The Personal Value of Code Review</title>
    <updated>2026-06-27T01:10:12.390Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="career-development" scheme="https://lichuanyang.top/en/tags/career-development/"/>
    <content>
      <![CDATA[<p>Today I want to talk about career choices for fresh graduates, primarily focused on backend development (the topic of role selection is also quite broad, and I may write a separate article about it if I get the chance). Over the past few years, I’ve worked in state-owned enterprises, large companies, and small companies, so I feel I have some real perspective on what different companies are like.</p><span id="more"></span><h2 id="Trade-offs-Across-Four-Dimensions"><a href="#Trade-offs-Across-Four-Dimensions" class="headerlink" title="Trade-offs Across Four Dimensions"></a>Trade-offs Across Four Dimensions</h2><p>Career choice ultimately comes down to two key decisions: city and company. Choosing a city is highly subjective. For example, as someone from the north, I don’t like the climate in southern cities, I want to be close to home, and I need the opportunities that a big city offers — so Beijing was almost the only option. As practical advice, I’d suggest considering fast-growing strong second-tier cities like Chongqing or Hefei, where you can ride the wave of urban development and increase your own value along the way.</p><p>Within the scope of Beijing, I’ll discuss company choices based on my own experience.</p><h2 id="Hukou-The-Ticket-to-a-Tier-1-City"><a href="#Hukou-The-Ticket-to-a-Tier-1-City" class="headerlink" title="Hukou: The Ticket to a Tier-1 City"></a>Hukou: The Ticket to a Tier-1 City</h2><p>When it comes to Beijing, one topic that inevitably comes up is the hukou (household registration). However, <strong>I strongly advise against letting a hukou decision dictate your entire life trajectory</strong> — for instance, joining a state-owned enterprise just for a hukou even though you have no desire to work in the system. Always prioritize thinking through your career development direction before worrying about hukou.</p><p>Many people agonize over public sector vs. private sector, big company vs. small company — but these are all surface-level choices. Even within the public sector, experiences can vary enormously. The right approach is to first clarify what you truly want, then seek out suitable opportunities. Knowing what you truly want isn’t easy for everyone, so let me break it down. Consider these questions:</p><ul><li>What do you most want from work? Money? Power? A sense of inner achievement? Or do you not expect anything from work at all?</li><li>How much of your life do you want work to occupy?</li><li>Among the resources gained through work (connections, information, etc.) and the capabilities developed, which do you feel more confident carrying into a different environment?</li><li>Do you prefer doing routine, administrative work, or do you want to continuously tackle challenging problems?</li></ul><p>For any of these questions, no answer is inherently better than another. Tackling difficult problems isn’t superior to doing routine work — it’s simply a choice based on personal values.</p><p>With any combination of these choices, you can find suitable opportunities. For instance, if you don’t expect much from work, don’t want it to dominate your life, and are willing to do routine work, some peripheral state-owned institutions could be a good fit. If you want power, are willing to let work take up significant time, and prefer to accumulate resources through work, consider core state-owned institutions with real authority. And if you want to earn money and prefer to develop personal capabilities, being a programmer is a solid choice.</p><p>Once you’ve thought through these questions, you can choose a company. Many times, we may not find a place that satisfies all our requirements. That’s okay — we can use job-hopping to address one part at a time, continuously gather information and improve our abilities, and find opportunities when we’re ready to seize them.</p><h2 id="Big-Tech-The-Value-and-Cost-of-the-Platform"><a href="#Big-Tech-The-Value-and-Cost-of-the-Platform" class="headerlink" title="Big Tech: The Value and Cost of the Platform"></a>Big Tech: The Value and Cost of the Platform</h2><p>Let me share how I approached my own key decisions based on my experience.</p><p>When I first graduated, my goal was to get a Beijing hukou while staying in the internet industry. I had two broad directions: competing for an SSP offer at an internet company — but my abilities at the time weren’t sufficient, so that path was hard to take. The other path was to find opportunities within state-owned enterprises where the business closely resembled internet scenarios, ideally consumer-facing products with large user bases and data volumes, so I wouldn’t fall too far behind industry-leading technologies. Fortunately, I found such an opportunity and successfully seized it. After joining, it was pretty much as I expected — although the company’s technical level wasn’t high and the corporate culture had many issues, the business scenarios were excellent and I had plenty of room to contribute. Through my work, I learned a great deal.</p><p>Next, I needed to address my big tech experience, so when I changed jobs, I only considered top-tier tech companies. The work itself was nothing special to speak of — although I wasn’t happy during that period, I achieved the goals I’d set, so there are no regrets.</p><p>For my next job change, I had three goals: significantly increase my income to close the gap with peers caused by my time at the state-owned enterprise; not work too much overtime; and have more relaxed interpersonal relationships. These three goals seem contradictory, don’t they? But with effort, I always managed to find suitable opportunities, and that’s how I ended up at my current company. Not all problems were solved perfectly, but they were addressed enough to make my work experience quite enjoyable.</p><p>Through my own experience, you can see that I always seemed to make contradictory demands — wanting both fish and bear paw — and fortunately, I managed to get both in the end. The truth is, there are so many companies in the world that no two conditions are necessarily mutually contradictory. <strong>There’s no need to give things up prematurely. For example, does being a programmer necessarily mean 996 work and giving up your life? Obviously not.</strong></p><p>I’d like to address two more specific questions that arise from the above.</p><p>First, whether to choose a Beijing hukou. Let me first say what I gained from it. I caught the wave of property price surges in 2015-2016, which significantly increased my family’s assets even though I had no financial planning awareness at the time. This no longer holds true today — under the policy of housing for living, not speculation, even Beijing properties can’t outperform the stock market. Second, some procedures became slightly more convenient to handle, which I consider negligible — I probably don’t deal with paperwork more than once a year, and an increasing number of procedures support remote processing. Third, I can rest assured about my children’s education in Beijing without worrying about future uncertainty. I say “uncertainty” because I think it’s highly uncertain whether non-Beijing hukou holders will still be barred from taking the gaokao in Beijing nearly twenty years from now, and equally uncertain how competitive Beijing’s gaokao will become. A Beijing hukou essentially fixes my future choices, but what those choices will actually amount to is hard to predict.</p><p>So currently, the value of a Beijing hukou is quite limited. If I could go back to 2015, I’d still choose a Beijing hukou without hesitation. But if I had to make this choice today in 2021, I’d most likely decline it.</p><p>Second, regarding big tech experience. My view is that you must have it, and the sooner the better, but there’s no need to stay at a big company for life. The value of big tech experience has been thoroughly discussed by many, so I won’t repeat it here. Feel free to reach out if you’re interested in discussing further.</p><h2 id="How-to-Choose"><a href="#How-to-Choose" class="headerlink" title="How to Choose"></a>How to Choose</h2><p>The above covers most of this article. One point I haven’t mentioned is the midlife crisis — some might wonder why. The reason is that I don’t think it’s a problem that can be solved through career choices alone. Some people think the public sector is stable — even if you coast every day, no one can touch you. But I don’t consider stability that depends on external conditions to be true stability. The public sector has had problems before in history. This path might give you a 99.99% chance of living peacefully for life, but if that 0.01% scenario occurs, what will you do? Being a programmer is different — perhaps only a 50% chance of staying comfortably at one company, but the capabilities developed through work give me the confidence to handle the other 50%. The only way to address the midlife crisis is self-improvement — whether building capabilities or accumulating resources, you must gain something from work to maintain your competitiveness.</p><p>I’m Liusha. You can connect with me through the WeChat public account (Mobility), <a href="https://lichuanyang.top/">my personal website</a>, and other channels.</p><p>Source: <a href="https://lichuanyang.top/en/posts/34931/">https://lichuanyang.top/en/posts/34931/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/34931/</id>
    <link href="https://lichuanyang.top/en/posts/34931/"/>
    <published>2021-03-30T10:36:15.000Z</published>
    <summary>Practical advice on city and company selection for fresh graduates, drawing from experiences at state-owned enterprises, big tech companies, and startups.</summary>
    <title>Hukou, Big Tech, High Salary, Life — Career Choices for Fresh Graduates</title>
    <updated>2026-06-27T03:48:25.554Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Architecture Design" scheme="https://lichuanyang.top/en/categories/Architecture-Design/"/>
    <category term="design-pattern" scheme="https://lichuanyang.top/en/tags/design-pattern/"/>
    <content>
      <![CDATA[<h2 id="Problem-Statement"><a href="#Problem-Statement" class="headerlink" title="Problem Statement"></a>Problem Statement</h2><p>Regarding the inputs and outputs of RPC interfaces, there has long been a school of thought that advocates wrapping all inputs and outputs into individual request and response classes. In this article, we will analyze whether this approach constitutes a sound design.</p><h2 id="Input-Parameter-Encapsulation-Analysis"><a href="#Input-Parameter-Encapsulation-Analysis" class="headerlink" title="Input Parameter Encapsulation Analysis"></a>Input Parameter Encapsulation Analysis</h2><p>Let’s start with inputs. I’ll state the conclusion first: encapsulation should only be done when there are excessive input parameters. In all other cases, there is absolutely no justification for encapsulation.</p><p>The reasons given for wrapping input parameters into a request class typically boil down to the following:</p><ul><li>Maintaining compatibility when interface parameters are modified</li><li>Defining a generic abstract request class for unified management</li></ul><p>First, the first point is a complete fallacy. The first time I encountered this claim, I was almost misled by it as well. But upon careful reflection, it becomes clear that this argument doesn’t hold at all.</p><p>Even with a request body approach, if the interface provider adds a mandatory field, the caller still needs to include this parameter to make the call. Compared to passing parameters directly, this doesn’t solve any problem. Furthermore, this kind of issue is fundamentally unsolvable because such situations should not arise in the first place. Interface providers have an obligation to ensure that subsequent modifications are forward-compatible. As for adding non-mandatory fields, even without wrapping in a request class, you can implement the new interface and maintain backward compatibility through an adapter pattern, and later asynchronously deprecate the old interface when the opportunity arises.</p><p>The second point does have some merit.</p><ul><li>For example, adding unified request parameters in an abstract class. I believe this is better handled in the framework layer rather than being exposed in business code.</li><li>Another example is adding unified parameter validation. But if that’s the only purpose, using an overly abstract class is inappropriate. Instead, each related business domain should have its own abstract class, since only that domain would have such requirements. Moreover, even without wrapping into a request class, such requirements are not difficult to implement, as the difficulty itself doesn’t lie in non-uniform input structures.</li></ul><p>The biggest problem with wrapping input parameters into a request class is that it destroys code readability and creates semantic ambiguity. A method like <code>queryById(long id)</code> has very clear semantics — a glance at the interface definition reveals the inputs and outputs. But after forced encapsulation, what inputs the interface actually requires becomes unclear. Additionally, if a request contains three parameters, can any combination of them return a result? This is also something you cannot determine just by looking at the interface. With normal parameter definitions, you can define a series of interfaces using the adapter pattern, making it easier for callers to understand.</p><p>The only condition for encapsulating input parameters is when there are too many parameters, and you need to use other means such as comments or documentation to specify the legal input values and upgrade strategies for the interface.</p><h2 id="Output-Result-Encapsulation-Analysis"><a href="#Output-Result-Encapsulation-Analysis" class="headerlink" title="Output Result Encapsulation Analysis"></a>Output Result Encapsulation Analysis</h2><p>Now let’s discuss outputs. Wrapping output results into a response body is somewhat more reasonable than wrapping input parameters, but it still shouldn’t be used indiscriminately. The general purpose of wrapping output results is to add a return status code layer.</p><h2 id="Interface-Segregation-Principle-Perspective"><a href="#Interface-Segregation-Principle-Perspective" class="headerlink" title="Interface Segregation Principle Perspective"></a>Interface Segregation Principle Perspective</h2><p>This essentially forces callers to understand the error conditions of the interface. We know that one of the fundamental design principles is the “Interface Segregation Principle,” which states that clients should not be forced to depend upon interfaces that they do not use. The situation here is analogous: if callers genuinely don’t care about the reasons for request success or failure, they shouldn’t be forced to understand them. For example, if I just want to query a count, there are only two possibilities — found or not found — and I don’t want to care about why it wasn’t found.</p><p>Therefore, when designing interfaces, you need to carefully consider whether the error conditions of an interface should be information that is provided externally. Only on this basis can you determine whether wrapping in a response object is appropriate or not.</p><h2 id="Conclusion-and-Recommendations"><a href="#Conclusion-and-Recommendations" class="headerlink" title="Conclusion and Recommendations"></a>Conclusion and Recommendations</h2><p>In summary, blindly wrapping all input and output parameters is never advisable. You must carefully consider when encapsulation is appropriate and when it is not. If you have any thoughts on this topic, feel free to discuss them with me.</p><p>Source: <a href="https://lichuanyang.top/en/posts/20888/">https://lichuanyang.top/en/posts/20888/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/20888/</id>
    <link href="https://lichuanyang.top/en/posts/20888/"/>
    <published>2021-03-16T10:34:56.000Z</published>
    <summary>An in-depth analysis of wrapping RPC interface inputs and outputs into request/response classes, discussing when encapsulation is appropriate and when it becomes over-engineering.</summary>
    <title>Is Encapsulating All RPC Interface Inputs and Outputs into Classes a Sound Design?</title>
    <updated>2026-06-27T03:50:56.858Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <content>
      <![CDATA[<p>Let me share my personal experience. At my first company right after graduation, I was actually doing infrastructure middleware development. But during that work, I realized I didn’t enjoy the feeling of being so far removed from real business. So when I later changed jobs, I deliberately chose a business-oriented department. Today, I want to explain why I made this choice.</p><span id="more"></span><h2 id="Why-I-Don’t-Do-Infrastructure-Development"><a href="#Why-I-Don’t-Do-Infrastructure-Development" class="headerlink" title="Why I Don’t Do Infrastructure Development"></a>Why I Don’t Do Infrastructure Development</h2><p>First, I want to address a common misconception: that infrastructure development makes it easier to learn and grow faster. I believe this is nothing more than wishful thinking by business developers. Infrastructure development isn’t as conducive to learning as people think, and business development isn’t as detrimental to learning as people think — I’ll elaborate on the reasons below. The reason many people hold this belief is largely because infrastructure development positions typically have higher hiring standards, so these individuals naturally have better learning ability and motivation.</p><p>In reality, the daily work of infrastructure development isn’t that different from business development — both involve working on requirements. The only difference is that business requirements come from product managers, while infrastructure requirements come from other developers, or from the infrastructure developer’s own thinking and ideas. This brings me to my first reason for not wanting to do infrastructure development: the source of requirements is unscientific. If you only do infrastructure development and are detached from actual business scenarios, your understanding of technology is likely to be biased, and things you want to build may just be self-indulgent exercises with no practical application value. Even when collecting requirements from business developers, there are similar issues — their suggestions may reflect only shallow, unconsidered thoughts. Moreover, business developers may not accurately understand what infrastructure components should actually handle; they may not distinguish between what should be solved by infrastructure and what they should handle themselves.</p><p>This brings me to my second reason for not wanting to do infrastructure development: unclear boundaries of responsibility. This is closely tied to the capability of the business teams you work with. Sometimes, when something on your end isn’t done well, the business team finds a way to work around it. Other times, even when something has nothing to do with you, you’re forced to deal with some upper-layer logic.</p><p>As you can see, both issues above are extremely dependent on colleagues’ abilities. So if you insist on doing infrastructure development, you must go to a company with strong technical talent — this directly affects your work happiness. The company choice also impacts another aspect: the nature of the work. Simply put, it’s a difference between building wheels versus improving open-source tools — these two types of work are worlds apart. Building wheels requires deep familiarity with various fundamental knowledge and can indeed promote growth — this is the origin of the idea that “infrastructure development helps you grow.” But this kind of work only exists at a handful of big companies. The vast majority of infrastructure work — improving open-source tools — is no different from business development. You only need to learn the ins and outs of the open-source tools to complete most of the work, with neither depth nor breadth. This is why I said earlier that infrastructure development doesn’t help with learning.</p><p>So, to do infrastructure development, company selection is crucial — it must be large AND technically strong; both conditions are essential. You might ask: why not just go to such a company? That’s fair, but it brings its own problem — my third reason for not wanting to do infrastructure development: the career choice is extremely narrow. In China, there are only two companies doing Java infrastructure well, and everyone knows what their corporate cultures are like. So if you don’t want to go to those companies, does that mean you can’t do it at all? Even if you’re willing, the massive supply-demand imbalance would lead to insane involution, similar to algorithm positions. Do you have enough confidence to win that game? In contrast, for business development, big tech experience is just a necessary learning experience and a highlight on your resume. Once you’re familiar with the entire standardized workflow at a big company, you can freely choose wherever you want to go.</p><h2 id="The-Value-of-Business-Development"><a href="#The-Value-of-Business-Development" class="headerlink" title="The Value of Business Development"></a>The Value of Business Development</h2><p>I’ve talked too much about infrastructure development above. Let me now get to the main topic: the value of business development. In my view, business development has improved my capabilities primarily in three areas: accumulation of business knowledge, expansion of technical breadth, and improvement of comprehensive abilities.</p><p>First, business knowledge — business knowledge is essentially an understanding of the real world. For me personally, learning about how different industries operate in my spare time is first and foremost an interesting thing to do. Second, continuously learning industry knowledge across different domains has noticeably improved my cognitive abilities. Can this ability be monetized? Of course. For example, when investing in stocks, the accumulation from work has given me the ability to analyze a company’s business model — I can clearly understand what makes a good company and a good stock at a deeper level than most people. Of course, I’m getting ahead of myself here — take it as food for thought. The key is to follow your interest and see if you’re willing to learn about things beyond technology.</p><p>Next, technical breadth. The biggest difference between business development and infrastructure development is that infrastructure development demands more depth, while business development demands more breadth. Business development requires familiarity with a wide variety of frameworks and middleware — not just surface-level knowledge, but enough depth to understand what problems they solve, what problems they introduce, and which scenarios they’re suited for. You need to comprehensively understand complex business scenarios and complex systems, and be able to quickly locate problems when they occur. Beyond knowledge accumulation, this also requires experience gained from real-world scenarios.</p><p>Then there’s the improvement of comprehensive abilities. Compared to infrastructure development, business development exercises a person’s general foundational capabilities more — communication, problem analysis and problem solving, and the ability to weigh trade-offs. These abilities are related to technology, but not so closely. To be extreme, even if you change careers in the future, these universal capabilities will be very helpful. I often think about what the difference is between learning to use an open-source tool and playing Honor of Kings. As a beginner, following an example to build a demo is like encountering a game for the first time and just playing around. As you get more familiar, some people become emotionless tool-switching machines, while others gradually explore the underlying implementation. Just like in gaming — some people play thousands of matches without ever understanding the difference between Lightning Dagger and Infinity Edge, while others explore the numerical mechanics of damage. So I think we shouldn’t view programmers as some uniquely special profession — it’s fundamentally no different from other professions. Most professions are just solving problems in different ways: lawyers rely on law, fund managers rely on understanding markets and fundamentals, and programmers rely on code. What we continuously develop through work is our problem-solving ability, and no one requires the solution to be limited to technology alone.</p><p>In business development, we must continuously accumulate capabilities in these three areas, so that we can build momentum over time and confidently face the midlife crisis. Of course, I’m not writing this to say that business development is inherently better — I mainly want to help everyone clearly analyze the differences between business development and infrastructure development, so you can make a rational choice.</p><p>If you have any thoughts, feel free to reach out and discuss with me.</p><p>Source: <a href="https://lichuanyang.top/en/posts/40071/">https://lichuanyang.top/en/posts/40071/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/40071/</id>
    <link href="https://lichuanyang.top/en/posts/40071/"/>
    <published>2021-03-12T11:27:36.000Z</published>
    <summary>From personal experience, analyzing the real differences between business development and infrastructure development, and dispelling the misconception that 'infrastructure development is better for growth'.</summary>
    <title>The Value of Business Development for Programmers</title>
    <updated>2026-06-27T03:51:55.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Architecture Design" scheme="https://lichuanyang.top/en/categories/Architecture-Design/"/>
    <category term="design-patterns" scheme="https://lichuanyang.top/en/tags/design-patterns/"/>
    <content>
      <![CDATA[<p>Hello everyone, I’m Liusha. Design patterns are something every programmer encounters frequently, but I believe many people still have questions about what design patterns really are. So today, let’s talk about this — the main goal is to help you understand the purpose of design patterns and the right mindset for approaching them.</p><span id="more"></span><h2 id="Prerequisites"><a href="#Prerequisites" class="headerlink" title="Prerequisites"></a>Prerequisites</h2><p>When we mention design patterns, the first thing to understand clearly is object-oriented thinking. I believe that even if you can’t describe it very precisely, you should already be fairly familiar with object-oriented programming.</p><p>Let me quickly review — object-oriented programming has four fundamental characteristics: encapsulation, abstraction, inheritance, and polymorphism.</p><p><strong>Encapsulation:</strong> Exposes only limited interfaces, authorizing external access. Centralizes logic, making it more controllable; improves readability and maintainability; also enhances usability.</p><p><strong>Abstraction:</strong> Hides the specific implementation of methods, so that callers only need to care about what functionality the method provides, without needing to know how those functions are implemented.</p><p><strong>Inheritance:</strong> The benefit is code reuse.</p><p><strong>Polymorphism:</strong> Subclasses can replace parent classes, improving code extensibility and reusability.</p><h2 id="What-Are-Design-Patterns"><a href="#What-Are-Design-Patterns" class="headerlink" title="What Are Design Patterns?"></a>What Are Design Patterns?</h2><p>Design patterns are a set of practical solutions or design approaches summarized for frequently encountered design problems in software development, based on fundamental design principles.</p><p>As you can see, design patterns are very practice-oriented, more concrete and executable compared to design principles.</p><p>Therefore, before learning about design patterns, you need to understand some basic design principles. These principles are the key to guiding us to write good code.</p><p>So, what is good code?</p><h2 id="What-Makes-Good-Code"><a href="#What-Makes-Good-Code" class="headerlink" title="What Makes Good Code"></a>What Makes Good Code</h2><p>This is actually quite hard to describe. We can try to summarize some characteristics of good code, such as:</p><p>Maintainable, extensible, readable, testable, reusable, and concise.</p><p>To achieve these standards, we need some basic design principles first.</p><h2 id="Fundamental-Design-Principles"><a href="#Fundamental-Design-Principles" class="headerlink" title="Fundamental Design Principles"></a>Fundamental Design Principles</h2><h2 id="SOLID"><a href="#SOLID" class="headerlink" title="SOLID"></a>SOLID</h2><h3 id="Single-Responsibility-Principle"><a href="#Single-Responsibility-Principle" class="headerlink" title="Single Responsibility Principle"></a>Single Responsibility Principle</h3><p>The description is simple: a class should only be responsible for one task or function. But actually doing this well is quite difficult.</p><p>To determine what counts as a single responsibility, we can set some simple criteria, such as: too many lines of code; too many dependent classes; too many private methods; difficulty naming the class; too many methods in the class with concentrated functionality. However, there’s no universal standard — you must consider the actual situation.</p><p>For example, consider this class:</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Student</span> &#123;</span><br><span class="line">    int id;</span><br><span class="line"><span class="title class_">String</span> name;</span><br><span class="line"><span class="title class_">String</span> age;</span><br><span class="line">...</span><br><span class="line"><span class="title class_">String</span> province;</span><br><span class="line"><span class="title class_">String</span> city;</span><br><span class="line"><span class="title class_">String</span> county;</span><br><span class="line"><span class="title class_">String</span> detailAddress;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>There are at least four fields related to addresses. Should we extract the address-related responsibilities into a separate Address class?</p><p>Actually, it depends on the situation. If the address properties are just pure display information, then placing them directly in the Student class is fine. But if there’s a complex logistics system behind them — meaning lots of complex logic related to addresses — then that part should be extracted separately.</p><p>In practice, we can start with a coarse-grained class to meet business requirements. As the business develops, if the coarse-grained class becomes increasingly large with more and more code, we can then split it into several finer-grained classes. This is called continuous refactoring.</p><h3 id="Open-Closed-Principle"><a href="#Open-Closed-Principle" class="headerlink" title="Open-Closed Principle"></a>Open-Closed Principle</h3><p>Open to extension, closed to modification.</p><p>Adding a new feature should be done by extending code on top of existing code (adding modules, classes, methods, properties, etc.), rather than modifying existing code (changing modules, classes, methods, properties, etc.).</p><p>The Open-Closed Principle is one of the harder principles to understand.</p><p>Questions like “What kind of code change is considered an ‘extension’? What kind of code change is considered a ‘modification’? How do we determine if the Open-Closed Principle is satisfied or violated? Does modifying code always mean violating the Open-Closed Principle?” are all difficult to answer.</p><p>Yet this principle is one of the most useful, because extensibility is one of the most important metrics for code quality.</p><p>In practice, we don’t need to agonize over whether a code change is a “modification” or an “extension,” nor worry too much about whether it violates the Open-Closed Principle. We should return to its original intent: as long as the existing code’s behavior isn’t broken, the Open-Closed Principle isn’t violated. A simple criterion is: when functionality hasn’t changed, if the corresponding unit tests aren’t broken, the change satisfies the Open-Closed Principle.</p><p>How to achieve openness to extension while being closed to modification also requires gradual learning and accumulation. Among the 23 classic design patterns, most exist to solve code extensibility problems, primarily following the Open-Closed Principle.</p><p>When writing code, you can assume changes won’t occur; when changes do arise, abstraction is needed to isolate future similar changes.</p><h3 id="Liskov-Substitution-Principle"><a href="#Liskov-Substitution-Principle" class="headerlink" title="Liskov Substitution Principle"></a>Liskov Substitution Principle</h3><p>Subclasses should be able to replace parent classes anywhere, while ensuring the original program’s logical behavior remains unchanged and correctness isn’t compromised.</p><p>The Liskov Substitution Principle has another more practical and instructive description: “Design By Contract.” In this description, the relationship between subclasses and parent classes can be replaced with the relationship between interfaces and implementations. In other words, implementation classes cannot violate the functionality, inputs, outputs, exceptions, etc. that the interface declares it will provide.</p><h3 id="Interface-Segregation-Principle"><a href="#Interface-Segregation-Principle" class="headerlink" title="Interface Segregation Principle"></a>Interface Segregation Principle</h3><blockquote><p>Clients should not be forced to depend upon interfaces that they do not use.</p></blockquote><p>The core element is that an “interface” should have as focused a responsibility as possible. Callers should only depend on the parts they need, rather than depending on or calling the entire “interface.”</p><p>Here, “interface” can refer to different meanings — for example, a set of API interfaces (i.e., a proxy we use).</p><p>For instance, in a project that handles business processes, generates data, and then needs to provide that data as a base service, you would provide two interfaces: one for processing internal processes and one as a base service for external access. This approach can help avoid accidental misuse to some extent.</p><p>You can also understand an interface as a single interface method, in which case it means each method should have a single responsibility.</p><p>The Interface Segregation Principle is somewhat similar to the Single Responsibility Principle, but there are slight differences. The Single Responsibility Principle targets the design of modules, classes, and interfaces. The Interface Segregation Principle, compared to the Single Responsibility Principle, focuses more on interface design. It provides a criterion for judging whether an interface has a single responsibility: indirectly determined by how callers use the interface. If callers only use part of the interface or some of its functionality, the interface design lacks a single responsibility.</p><h3 id="Dependency-Inversion-Principle"><a href="#Dependency-Inversion-Principle" class="headerlink" title="Dependency Inversion Principle"></a>Dependency Inversion Principle</h3><blockquote><p>High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.</p></blockquote><p>The division between high-level and low-level modules simply means that in the call chain, the caller belongs to the high level, and the callee belongs to the low level.</p><p>This principle is more targeted at framework development. In business development, high-level modules depending on low-level modules is actually perfectly fine.</p><p>Taking Tomcat as an example: Tomcat calls web programs to run, so Tomcat is the high-level module.</p><p>Tomcat and application code don’t have direct dependencies on each other; both depend on the same “abstraction,” which is the Servlet specification. The Servlet specification doesn’t depend on specific Tomcat container or application implementation details, while the Tomcat container and application depend on the Servlet specification.</p><h3 id="KISS"><a href="#KISS" class="headerlink" title="KISS"></a>KISS</h3><p>Keep It Simple and Stupid.</p><p>Keep It Short and Simple.</p><p>Keep It Simple and Straightforward.</p><p>The KISS principle has different formulations, but they all convey roughly the same meaning: keep code as simple as possible.</p><p>“Simplicity” is actually very subjective. Code review is a good indirect way to evaluate whether code is simple. If colleagues reviewing your code find many parts hard to understand, the code is likely not simple enough.</p><p>Some simple tips: Don’t reinvent the wheel; avoid over-engineering, whether for uncertain future extensibility or minor performance differences.</p><p>For example, many methods in StringUtils, in a specific scenario, could likely be implemented with better performance because the general concerns that StringUtils handles don’t need to be considered in specific cases. But should we really do that?</p><h3 id="DRY-Principle-Don’t-Repeat-Yourself"><a href="#DRY-Principle-Don’t-Repeat-Yourself" class="headerlink" title="DRY Principle (Don’t Repeat Yourself)"></a>DRY Principle (Don’t Repeat Yourself)</h3><p>This principle is simple in itself, but note that the “repeat” here is different from simple code duplication. For example, two unrelated methods that happen to have identical implementations are not considered a DRY violation because semantically they’re not duplicating. Conversely, two methods with completely different names and implementations — like <code>isValidIp()</code> and <code>checkIpValid()</code> — that do the same thing are also considered DRY violations.</p><p>Also note execution duplication, for example:</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">run1</span>(<span class="params"></span>) &#123;</span><br><span class="line"><span class="title function_">a</span>();</span><br><span class="line"><span class="title function_">b</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">run2</span>(<span class="params"></span>) &#123;</span><br><span class="line"><span class="title function_">a</span>();</span><br><span class="line"><span class="title function_">c</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">main</span> () &#123;</span><br><span class="line"><span class="title function_">run1</span>();</span><br><span class="line"><span class="title function_">run2</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Although the code appears well-encapsulated, when the <code>main</code> method is executed, method <code>a</code> is called twice. If <code>a</code> is a method with high overhead, this could be problematic.</p><h3 id="Law-of-Demeter"><a href="#Law-of-Demeter" class="headerlink" title="Law of Demeter"></a>Law of Demeter</h3><blockquote><p>Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.</p></blockquote><p>“High cohesion, loose coupling” is a relatively universal design concept that can guide the design and development of code at different granularities — systems, modules, classes, and functions.</p><p>High cohesion means that related functionality should be placed in the same class (module, system), while unrelated functionality should not be placed in the same class (module, system).</p><p>Loose coupling means that in code, the dependency relationships between classes (modules, systems) are simple and clear, depending only on what’s necessary.</p><p>For example, qwerty’s previous handling of homework-related logic required understanding the implementation details of each homework type, which was a violation of the Law of Demeter. The correct approach is to define a common model on the homework side, encapsulating the different logic of various homework types within the model, so qwerty only needs to understand this model.</p><p>For these design principles, there’s no need to spend too much effort on the exact wording. What’s more important is understanding their underlying ideas — understanding why these principles help us write code that is “maintainable, extensible, readable, testable, reusable, and concise.”</p><p>Next, we can talk specifically about design patterns.</p><h2 id="Common-Design-Patterns"><a href="#Common-Design-Patterns" class="headerlink" title="Common Design Patterns"></a>Common Design Patterns</h2><p>There are 23 classic design patterns. With the evolution of programming languages, some design patterns (like Singleton) have become outdated or even anti-patterns, some have been built into programming languages (like Iterator), and some new patterns have emerged (like Monostate).</p><p><strong>Creational Patterns (5):</strong> Factory Method, Abstract Factory, Singleton, Builder, Prototype.</p><p><strong>Structural Patterns (7):</strong> Adapter, Decorator, Proxy, Facade, Bridge, Composite, Flyweight.</p><p><strong>Behavioral Patterns (11):</strong> Strategy, Template Method, Observer, Iterator, Chain of Responsibility, Command, Memento, State, Visitor, Mediator, Interpreter.</p><p>Let’s focus on some of the more important ones.</p><h2 id="Simple-Factory-Factory-Method"><a href="#Simple-Factory-Factory-Method" class="headerlink" title="Simple Factory &amp; Factory Method"></a>Simple Factory &amp; Factory Method</h2><p>For example, consider a feature that creates different parsers based on file format to read configuration into <code>RuleConfig</code>. The Simple Factory implementation looks like this:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigSource</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> RuleConfig <span class="title function_">load</span><span class="params">(String ruleConfigFilePath)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">ruleConfigFileExtension</span> <span class="operator">=</span> getFileExtension(ruleConfigFilePath);</span><br><span class="line">    <span class="type">IRuleConfigParser</span> <span class="variable">parser</span> <span class="operator">=</span> RuleConfigParserFactory.createParser(ruleConfigFileExtension);</span><br><span class="line">    <span class="keyword">if</span> (parser == <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">InvalidRuleConfigException</span>(</span><br><span class="line">              <span class="string">&quot;Rule config file format is not supported: &quot;</span> + ruleConfigFilePath);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="variable">configText</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="comment">//从ruleConfigFilePath文件中读取配置文本到configText中</span></span><br><span class="line">    <span class="type">RuleConfig</span> <span class="variable">ruleConfig</span> <span class="operator">=</span> parser.parse(configText);</span><br><span class="line">    <span class="keyword">return</span> ruleConfig;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> String <span class="title function_">getFileExtension</span><span class="params">(String filePath)</span> &#123;</span><br><span class="line">    <span class="comment">//...解析文件名获取扩展名，比如rule.json，返回json</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;json&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigParserFactory</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> IRuleConfigParser <span class="title function_">createParser</span><span class="params">(String configFormat)</span> &#123;</span><br><span class="line">    <span class="type">IRuleConfigParser</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&quot;json&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">JsonRuleConfigParser</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;xml&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">XmlRuleConfigParser</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;yaml&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">YamlRuleConfigParser</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;properties&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">PropertiesRuleConfigParser</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> parser;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This creates a factory class specifically for “producing” parsers.</p><p>Placing the parser creation logic directly in the <code>load</code> method would also work. The reason to separate it out is to isolate the creation logic. Therefore, the factory pattern is only used when the creation logic is complex.</p><p>The above implementation can be further optimized to eliminate the large number of if-else statements:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigSource</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> RuleConfig <span class="title function_">load</span><span class="params">(String ruleConfigFilePath)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">ruleConfigFileExtension</span> <span class="operator">=</span> getFileExtension(ruleConfigFilePath);</span><br><span class="line"></span><br><span class="line">    <span class="type">IRuleConfigParserFactory</span> <span class="variable">parserFactory</span> <span class="operator">=</span> RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);</span><br><span class="line">    <span class="keyword">if</span> (parserFactory == <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">InvalidRuleConfigException</span>(<span class="string">&quot;Rule config file format is not supported: &quot;</span> + ruleConfigFilePath);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">IRuleConfigParser</span> <span class="variable">parser</span> <span class="operator">=</span> parserFactory.createParser();</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="variable">configText</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="comment">//从ruleConfigFilePath文件中读取配置文本到configText中</span></span><br><span class="line">    <span class="type">RuleConfig</span> <span class="variable">ruleConfig</span> <span class="operator">=</span> parser.parse(configText);</span><br><span class="line">    <span class="keyword">return</span> ruleConfig;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> String <span class="title function_">getFileExtension</span><span class="params">(String filePath)</span> &#123;</span><br><span class="line">    <span class="comment">//...解析文件名获取扩展名，比如rule.json，返回json</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;json&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//因为工厂类只包含方法，不包含成员变量，完全可以复用，</span></span><br><span class="line"><span class="comment">//不需要每次都创建新的工厂类对象，所以，简单工厂模式的第二种实现思路更加合适。</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigParserFactoryMap</span> &#123; <span class="comment">//工厂的工厂</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map&lt;String, IRuleConfigParserFactory&gt; cachedFactories = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">static</span> &#123;</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;json&quot;</span>, <span class="keyword">new</span> <span class="title class_">JsonRuleConfigParserFactory</span>());</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;xml&quot;</span>, <span class="keyword">new</span> <span class="title class_">XmlRuleConfigParserFactory</span>());</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;yaml&quot;</span>, <span class="keyword">new</span> <span class="title class_">YamlRuleConfigParserFactory</span>());</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;properties&quot;</span>, <span class="keyword">new</span> <span class="title class_">PropertiesRuleConfigParserFactory</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> IRuleConfigParserFactory <span class="title function_">getParserFactory</span><span class="params">(String type)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (type == <span class="literal">null</span> || type.isEmpty()) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">IRuleConfigParserFactory</span> <span class="variable">parserFactory</span> <span class="operator">=</span> cachedFactories.get(type.toLowerCase());</span><br><span class="line">    <span class="keyword">return</span> parserFactory;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This is the Factory Method Pattern. The Factory Method Pattern essentially creates a simple factory for the factory class — a factory of factories — to create factory class objects.</p><p>The principles and implementations of the factory pattern are quite simple. In practice, there are many details worth exploring, but we won’t go into them here. The key point we need to understand is when to use this pattern and what problem it solves.</p><p>For the factory pattern, the application scenario is when the creation logic is complex — for example, when there are lots of if-else statements to dynamically decide what object to create; or when creating a single object itself is a complex process and the caller doesn’t need to know how to create it.</p><h2 id="Builder"><a href="#Builder" class="headerlink" title="Builder"></a>Builder</h2><p>The Builder Pattern is widely used. If you open any project, you’ll find many <code>XXXBuilder</code> classes.</p><p>The Builder Pattern is used when a class has multiple configurable options and the user can freely choose which ones to configure.</p><p>If you try to achieve this with different constructors, you’d face an endless number of combinations.</p><p>And if you use setter methods for configuration, the class completely loses control over its configuration options — callers can invoke setters anytime, anywhere. Additionally, if you need to perform validation after configuration is complete, this approach can’t accommodate that.</p><p>The solution for this situation is the Builder Pattern:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ResourcePoolConfig</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ResourcePoolConfig</span>.Builder()</span><br><span class="line"> .setName(<span class="string">&quot;dbconnectionpool&quot;</span>) </span><br><span class="line"> .setMaxTotal(<span class="number">16</span>) </span><br><span class="line"> .setMaxIdle(<span class="number">10</span>) </span><br><span class="line"> .setMinIdle(<span class="number">12</span>)</span><br><span class="line"> .build();</span><br></pre></td></tr></table></figure><p>The implementation is relatively straightforward.</p><p>The factory pattern is used to create a family of related but different objects, while the Builder Pattern is used to customize the same object in different ways.</p><h2 id="Singleton"><a href="#Singleton" class="headerlink" title="Singleton"></a>Singleton</h2><p>As for what a singleton is and how to implement one — as a classic interview question — I won’t elaborate further. I’ll only discuss the problems with singletons and why they’ve gradually become an anti-pattern.</p><p>Problems with singletons:</p><ul><li>Hidden dependencies between classes;</li><li>Dependencies point directly to singleton implementations, violating the principle of depending on interfaces rather than implementations;</li><li>Poor testability: cannot be mocked (in Mockito); global state can cause tests to interfere with each other;</li><li>Doesn’t support parameterized constructors.</li></ul><p>However, the advantage of the Singleton Pattern is that in some scenarios, it’s simpler and more convenient to use. So there’s no need to categorically rule out using singletons.</p><h2 id="Proxy-Decorator"><a href="#Proxy-Decorator" class="headerlink" title="Proxy &amp; Decorator"></a>Proxy &amp; Decorator</h2><p>The Proxy Pattern has the proxy class implement the same interface as the target class, responsible for adding additional logic before and after business code execution (such as performance monitoring, counting, etc.), then delegating to the target class to complete the business function, thereby achieving decoupling between business code and framework code.</p><p>The Decorator Pattern achieves functionality enhancement through inheritance of the decorated class. For example, Java’s <code>InputStream</code> series of classes are applications of the Decorator Pattern.</p><p>At the implementation level, the Proxy Pattern and Decorator Pattern are almost identical, but their intents differ.</p><p>In the Proxy Pattern, the proxy class adds functionality unrelated to the original class, while in the Decorator Pattern, the decorator class adds enhanced functionality related to the original class.</p><p>Again, for design patterns, there’s no need to nitpick about what each pattern exactly is. As long as you know such a technique exists and can handle similar scenarios, that’s sufficient.</p><h2 id="Adapter"><a href="#Adapter" class="headerlink" title="Adapter"></a>Adapter</h2><p>The Adapter Pattern is straightforward — let’s look at an example directly.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">debug</span><span class="params">(String msg)</span> &#123; </span><br><span class="line">logger.log(FQCN, Level.DEBUG, msg, <span class="literal">null</span>); </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">debug</span><span class="params">(String format, Object arg)</span> &#123; </span><br><span class="line"><span class="keyword">if</span> (logger.isDebugEnabled()) &#123; </span><br><span class="line"><span class="type">FormattingTuple</span> <span class="variable">ft</span> <span class="operator">=</span> MessageFormatter.format(format, arg); </span><br><span class="line">logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); </span><br><span class="line">&#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Application scenarios include:</p><ul><li>Maintaining backward compatibility with old interfaces;</li><li>Handling different inputs;</li><li>Wrapping flawed interface designs;</li><li>…</li></ul><h2 id="Facade"><a href="#Facade" class="headerlink" title="Facade"></a>Facade</h2><p>The Facade Pattern provides a unified set of interfaces for subsystems, defining a set of high-level interfaces to make subsystems easier to use. This mainly concerns how large an interface’s granularity should be, balancing the generic usability and ease of use of interfaces.</p><p>Typically, interfaces with smaller granularity will have better generic usability, but callers would need to invoke multiple methods, which is inevitably less convenient. Conversely, if the interface granularity is too large, there will be insufficient generic usability.</p><p>The Facade Pattern solves this problem. I work in online education, and one scenario is displaying various types of questions on the B-side — for example, pre-class questions, in-class questions, and post-class questions. Each category has many specific types, and each type has different data sources.</p><p>Here, we need to balance the generic usability and practical usability of interfaces. The Facade Pattern perfectly addresses our problem. First, we define a common model including question stems, reference answers, user responses, etc., then implement a set of interfaces for specific types. On top of that, based on business scenarios, we encapsulate some facade layers that simply combine data from the subsystems.</p><h2 id="Strategy"><a href="#Strategy" class="headerlink" title="Strategy"></a>Strategy</h2><p>Define a family of algorithm classes, encapsulate each algorithm separately, and allow them to be interchangeable.</p><p>The implementation involves defining a strategy interface and a set of strategies implementing this interface. At runtime, which strategy to execute can be dynamically chosen based on certain conditions.</p><h2 id="Template-Method"><a href="#Template-Method" class="headerlink" title="Template Method"></a>Template Method</h2><p>The Template Method Pattern defines an algorithm skeleton in one method and defers certain steps to subclasses for implementation. The Template Method Pattern allows subclasses to redefine certain steps of the algorithm without changing its overall structure.</p><p>For example, Java’s <code>AbstractList.addAll</code> method is a template method:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">addAll</span><span class="params">(<span class="type">int</span> index, Collection&lt;? extends E&gt; c)</span> &#123;</span><br><span class="line">    rangeCheckForAdd(index);</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">modified</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">for</span> (E e : c) &#123;</span><br><span class="line">        add(index++, e);</span><br><span class="line">        modified = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> modified;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> index, E element)</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Observer"><a href="#Observer" class="headerlink" title="Observer"></a>Observer</h2><p>Define a one-to-many dependency between objects. When one object’s state changes, all dependent objects are automatically notified. This is also known as the publish-subscribe pattern.</p><p>The Observer Pattern is a relatively abstract pattern. Depending on different application scenarios and requirements, there are completely different implementation approaches. But the underlying idea is easy to understand, so I won’t include code here. Our use of message queues (MQ) is essentially using the Observer Pattern.</p><h2 id="Chain-of-Responsibility"><a href="#Chain-of-Responsibility" class="headerlink" title="Chain of Responsibility"></a>Chain of Responsibility</h2><p>Decouple the sending and receiving of requests, allowing multiple receivers the opportunity to handle a request. String these receivers into a chain and pass the request along the chain until a receiver on the chain can handle it.</p><p>In practice, a variant of the Chain of Responsibility Pattern is also widely used — where every receiver on the chain processes the request.</p><p>For example, we have a data assembly logic that’s an application of the Chain of Responsibility variant:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">fillUserDataCollection</span><span class="params">(Map&lt;Integer, UserDataCollection&gt; userExpandMap, Map&lt;Integer, UserDataCollection&gt; originUserExpandMap)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> !fillingServices</span><br><span class="line">                .stream()</span><br><span class="line">                .map(filler -&gt; filler.fillUserDataWithFallback(userExpandMap, originUserExpandMap))</span><br><span class="line">                .collect(toSet())</span><br><span class="line">                .contains(<span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><code>UserDataCollection</code> contains many fields, and each <code>fillingService</code> is responsible for supplementing a portion of the data.</p><p>The core idea of the Chain of Responsibility is decomposition — breaking large classes into smaller ones. This is also one of the important means of managing code complexity.</p><h2 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h2><p>For writing good code, the most critical thing is to first build a mental awareness of what good code is and have the desire to write good code. On top of that, you need a thorough understanding of object-oriented thinking and various design principles, knowing why good code should adhere to these principles.</p><p>In practice, the best way to write good code is continuous refactoring. When business development is still unclear at the beginning, it’s normal that code can’t be designed perfectly. As the business develops, we gradually realize the problems in our code. At this point, we can look through design patterns for ready-made solutions and apply them to our own projects.</p><p>Finally, I should mention that much of the content in this article actually comes from the Geek Time column “The Beauty of Design Patterns.” I think this is the best column on design patterns and recommend checking it out.</p><p><img src="/en/img/design.jpg" alt="design"></p><p>Source: <a href="https://lichuanyang.top/en/posts/58527/">https://lichuanyang.top/en/posts/58527/</a></p><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-What-if-I-learn-design-patterns-but-forget-them-without-using-them"><a href="#Q-What-if-I-learn-design-patterns-but-forget-them-without-using-them" class="headerlink" title="Q: What if I learn design patterns but forget them without using them?"></a>Q: What if I learn design patterns but forget them without using them?</h3><p>This is normal. The key is to <strong>use the right learning approach</strong>. Don’t try to memorize all 23 patterns’ definitions and structures in one go — that’s guaranteed to fade. The correct path: first understand the underlying ideas of SOLID and other design principles, then when you encounter design problems in real projects, refer back to design patterns for ready-made solutions. Apply them purposefully, refactor continuously. As you use them more, the mapping between scenarios and solutions becomes internalized — no deliberate memorization needed.</p><h3 id="Q-Which-design-pattern-is-the-most-commonly-used"><a href="#Q-Which-design-pattern-is-the-most-commonly-used" class="headerlink" title="Q: Which design pattern is the most commonly used?"></a>Q: Which design pattern is the most commonly used?</h3><p>Among creational patterns, <strong>Factory</strong> and <strong>Builder</strong> are used most — you’ll find them in dependency injection and object construction in nearly any project of moderate complexity. Among structural patterns, <strong>Proxy</strong> (e.g., AOP, RPC frameworks), <strong>Adapter</strong> (interface compatibility), and <strong>Decorator</strong> (e.g., Java IO streams) are the most common. Among behavioral patterns, <strong>Strategy</strong>, <strong>Template Method</strong>, and <strong>Chain of Responsibility</strong> appear most often in real-world business code, since they directly correspond to three high-frequency scenarios: “choose an algorithm based on conditions,” “define a processing skeleton,” and “chain-based request handling.”</p><h3 id="Q-Which-is-more-important-—-design-principles-or-design-patterns"><a href="#Q-Which-is-more-important-—-design-principles-or-design-patterns" class="headerlink" title="Q: Which is more important — design principles or design patterns?"></a>Q: Which is more important — design principles or design patterns?</h3><p>Design principles are more important. Principles are the “why”; patterns are the “how.” SOLID, KISS, DRY, and the Law of Demeter guide you in judging the trade-offs of code quality — they are the common foundation for all good code. Design patterns are merely concrete implementations of these principles in certain typical scenarios. Understand the principles, and you can write good code even without remembering a specific pattern’s implementation. The reverse — memorizing patterns without understanding principles — leads to cargo-cult application.</p><h3 id="Q-Why-has-the-Singleton-pattern-become-an-anti-pattern"><a href="#Q-Why-has-the-Singleton-pattern-become-an-anti-pattern" class="headerlink" title="Q: Why has the Singleton pattern become an anti-pattern?"></a>Q: Why has the Singleton pattern become an anti-pattern?</h3><p>The main issues: Singletons <strong>hide dependencies between classes</strong> — calling code depends on <code>Singleton.getInstance()</code> without revealing from the interface that global state is involved. They have <strong>poor testability</strong> — Singletons can’t be mocked, and global state can cause cross-test interference. They <strong>don’t support parameterized constructors</strong> — limiting flexibility. That said, this doesn’t mean Singletons are categorically unusable. When you genuinely need a single process-wide instance and complex testing isn’t involved, a Singleton remains the simplest choice. The key is understanding its costs, not blindly believing “Singleton &#x3D; bad.”</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/58527/</id>
    <link href="https://lichuanyang.top/en/posts/58527/"/>
    <published>2021-03-04T11:26:30.000Z</published>
    <summary>Starting from the four fundamental characteristics of object-oriented programming, this article helps programmers truly understand the essence of design patterns and the right mindset for learning them.</summary>
    <title>What Exactly Are Design Patterns? A Deep Dive into Their Principles</title>
    <updated>2026-06-27T02:41:11.326Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <content>
      <![CDATA[<p>Hi everyone, I’m Liusha, a programmer with nearly six years of work experience. My career has spanned companies with very different cultures and atmospheres. I’ve collaborated with many different types of people and observed numerous inefficiencies. At the same time, I’ve always felt that my own development efficiency is quite high. Throughout my career, I’ve rarely had to handle work tasks outside of my eight-hour workday. Even when forced to work overtime due to company policy, I was usually executing my own learning plan or doing some deep thinking. So, I’d like to share a systematic summary of development efficiency with everyone.</p><span id="more"></span><p>First, let me describe two highly inefficient scenarios:</p><ul><li>Xiao A encounters an unfamiliar error during development, so he carries his laptop over to find the team expert Xiao X. Xiao X then works hard to collect all the necessary information. Meanwhile, Xiao A just stands there watching, occasionally chatting with Xiao X and nearby Xiao Y.</li><li>Xiao C receives a large requirement, quickly skims through it, and starts coding. Halfway through development, he realizes he doesn’t understand a certain point in the product spec and goes to ask the product manager. During the conversation, the product manager remembers other things and starts talking about those too. After a long discussion, Xiao C discovers that most of his previous work was wasted.</li></ul><p>While these scenarios are somewhat exaggerated, similar situations truly happen all the time. There are many causes for these phenomena, and we’ll analyze them thoroughly today. Before diving into efficiency issues, we need to first look at what programmers do daily. The activities that have the greatest impact on work efficiency can be mainly divided into three categories:</p><ol><li>Daily development</li><li>Communication</li><li>Troubleshooting and problem-solving</li></ol><p>Next, we’ll analyze each of these three areas and discuss how to improve efficiency.</p><h2 id="Daily-Development"><a href="#Daily-Development" class="headerlink" title="Daily Development"></a>Daily Development</h2><p>What situations in daily development cause efficiency to drop? Let me list a few:</p><ul><li>Rework behavior like Xiao C mentioned above</li><li>Large amounts of repetitive labor in daily work, such as writing SQL, writing boilerplate code</li><li>A seemingly small change that turns out to require changes here and there; worse still, you don’t even know about “the other changes,” so everything breaks when you make the change</li><li>Lack of personal focus, constantly getting distracted</li></ul><p>You can see that most of these are related to code design. Regarding design, it really comes down to two points: first, you must design, and second, you must design well.</p><p>This means, first of all, you need to know that there should be a design phase before development. Software engineering is a science — it has steps like high-level design and detailed design for good reason. How detailed should the design be before you start coding? My view is: the more detailed the better. In theory, everything except actually writing the code can be done during the design phase.</p><p>As for whether the design is good or not, an important evaluation criterion is how easy it is to make changes later. If a single change in requirements causes modifications in multiple places in the code, that’s a poor design. Improving this requires learning about design patterns and architecture — there are no shortcuts.</p><p>Additionally, regarding the focus issue, everyone can certainly use tools like Pomodoro timers. But tools can only ever be supplementary; the most important thing is to improve your own self-discipline on a personal level.</p><h2 id="Communication"><a href="#Communication" class="headerlink" title="Communication"></a>Communication</h2><p>Now let’s talk about communication. Communication can be proactive or reactive. Proactive communication is when you initiate it — going to ask others questions or discuss things. Reactive communication is when others initiate — coming to ask you questions or discuss matters.</p><p>The main problem with proactive communication is that conversations tend to drift off topic. Here are two suggestions: first, before initiating communication, you must clearly think about what you want to achieve through this communication. Don’t go into a conversation with just a vague idea — that kind of communication is the biggest time killer. Second, you can make a communication checklist. Topic drift during conversations is actually a common phenomenon. I don’t think there’s a need to deliberately try to prevent it. The key is to ensure you get results on what you originally wanted to discuss, which requires a checklist you can use to verify.</p><p>Reactive communication is something that easily disrupts work rhythm, but in many cases it’s unavoidable — sudden production issues to handle, your manager needing to talk to you, etc. What we can do is reduce unnecessary reactive communication. For example, if you provide a feature, write good documentation and comments so others don’t need to ask you to understand it. Method names, return values, and other conventions should be established within the team — when everyone follows them, there’s no need to ask to understand things.</p><p>Another point is to break down tasks well — do several small tasks instead of one big one. This way, when you’re forced to interrupt your work, the cost of “context switching” will be lower.</p><h2 id="Troubleshooting-and-Problem-Solving"><a href="#Troubleshooting-and-Problem-Solving" class="headerlink" title="Troubleshooting and Problem-Solving"></a>Troubleshooting and Problem-Solving</h2><p>This is also a very important area. For some people, the time spent finding and fixing bugs may far exceed the time spent developing new features. For this, our main goal is to reduce the difficulty and time of troubleshooting. We can approach this from several aspects:</p><ol><li><p>Try to expose problems as early as possible, for example by writing unit tests. The same problem appearing in a small module versus a complex system — the troubleshooting difficulty is self-evident.</p></li><li><p>You need sufficient information, such as logs and various types of monitoring data. It’s like a physical examination — you first need to see the abnormal indicators before you can do targeted, detailed investigation. Otherwise, with no indicators to go on, you’re just blindly guessing.</p></li><li><p>Then there’s the troubleshooting approach itself. Everyone needs to gradually accumulate experience and establish a relatively fixed routine for troubleshooting problems. For example, my habit is to first scan all available information when I encounter a problem, identify abnormal indicators, use search engines or others to understand the indicators if I can’t read them, think about possible causes based on the indicators, and then verify step by step. An important point here is knowing what kinds of problems to think through yourself versus what kinds of problems should be handled with the help of others or search engines. Xiao A’s approach described above is very undesirable — when you ask others or use search engines, the question should be very specific. If you just casually send over an error message, others first need to spend a lot of time collecting information. A problem that could be solved in one hour, if you insist on asking someone else, could easily end up costing two people an entire afternoon.</p><p>What does “specific” mean? For example, if you encounter a ClassNotFoundException and you have no idea what it means, then searching for it is perfectly fine. But if you already know it means the class couldn’t be found, then searching again is meaningless. Instead, you should think about why this class couldn’t be found and what possible causes there might be.</p></li></ol><p>Besides the above, there’s actually another category: the deliberate slacking off that many people choose in the 996 work culture. On this point, I still suggest that everyone use the time freed up by finishing work quickly to improve themselves — slacking off doesn’t help. Another point is to vote with your feet — actively seek out companies and teams without an overtime culture, and use your efforts to help them develop better.</p><p>If you have thoughts, feel free to exchange ideas in the comments section or on the public account (Mobility).</p><p>Original article: <a href="https://lichuanyang.top/posts/3423/">https://lichuanyang.top/posts/3423/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/3423/">https://lichuanyang.top/en/posts/3423/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/3423/</id>
    <link href="https://lichuanyang.top/en/posts/3423/"/>
    <published>2021-02-01T09:38:11.000Z</published>
    <summary>A programmer with nearly six years of experience shares systematic methods for improving development efficiency, emphasizing completing work efficiently within eight hours.</summary>
    <title>My Tips for Improving Development Efficiency</title>
    <updated>2026-06-27T03:51:55.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Architecture Design" scheme="https://lichuanyang.top/en/categories/Architecture-Design/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <category term="design-patterns" scheme="https://lichuanyang.top/en/tags/design-patterns/"/>
    <category term="software-engineering" scheme="https://lichuanyang.top/en/tags/software-engineering/"/>
    <content>
      <![CDATA[<p>For a programmer, the word most often mentioned in daily work is probably “complex” — this code is too complex, this logic is too complex. So, in this article, let’s take a thorough look at where “complexity” actually comes from and how to avoid it.</p><span id="more"></span><h2 id="The-Origins-of-Complexity"><a href="#The-Origins-of-Complexity" class="headerlink" title="The Origins of Complexity"></a>The Origins of Complexity</h2><p>Let’s first list what we’re actually talking about when we say something is “complex”:</p><ul><li>No module division, just a massive blob of code sitting there</li><li>Large amounts of duplicate code</li><li>The overall logic of the code is incomprehensible</li><li>A seemingly simple requirement that turns out to require changes here and there when you try to modify it</li><li>Code can’t be migrated or reused</li><li>Mysterious bugs keep appearing all the time</li><li>…</li></ul><p>Many of these problems listed above are actually cause and effect of each other. For example, poor module division leads to incomprehensible code logic, which also leads to code that can’t be reused. Code that can’t be reused leads to large amounts of duplicate code, and duplicate code further leads to incomprehensible code logic.</p><p>So, below let’s try to organize these problems and see where the real issues lie.</p><p>I think we can first roughly categorize code complexity problems into two types: problems with the code itself, and problems with the code’s expressiveness of the real world. The former mainly concerns the overall cleanliness of the code and how easy it is to maintain continuously; the latter mainly affects how easy the code is to understand and whether others need a lot of background knowledge when reading the code.</p><p>Regarding problems with the code itself, in my observation, the primary reason for excessive code complexity is unclear module splitting, or even no module splitting at all. The problems listed above — duplicate code, inability to reuse, needing to modify the same logic in multiple places — all fundamentally stem from module splitting issues. There are also some naming problems that make code even more unmaintainable.</p><p>Regarding the code’s expressiveness of the real world, I think a big problem is that code is too disconnected from reality, making it so that business background knowledge alone isn’t enough to understand how the code is implemented. At the same time, when you implement real-world requirements with a design that doesn’t match reality, every step becomes difficult. For example, consider a business that displays user flight tickets — if all upstream airlines use “user ID number” as the data key, what should your key be? Should it also be the ID number? If you do that, and then if you implement database sharding, you can imagine how complex implementing a basic function like “query user’s most recent ticket” would become.</p><p>Once we reason through to this point, we can see that these issues basically cover all the problems listed at the beginning. Issues like frequent bugs are actually a manifestation of logical complexity. Because the logic is complex and incomprehensible, bugs are inevitable.</p><p>Additionally, there’s one more issue: each of the points mentioned above has an exponential amplification effect as the program scale increases. Many people can probably relate — if you have a 5-day effort requirement versus five independent 1-day effort requirements, the first one will actually take much more time and produce many more bugs in the end.</p><p>So, next let’s provide some solutions for these issues.</p><h2 id="Solutions"><a href="#Solutions" class="headerlink" title="Solutions"></a>Solutions</h2><h2 id="Module-Splitting"><a href="#Module-Splitting" class="headerlink" title="Module Splitting"></a>Module Splitting</h2><p>First and foremost, the top priority is doing module splitting well. You need to do splitting well at every level — for example, how to define microservice boundaries, what content goes into which class, what content goes into which method. It’s no exaggeration to say that good module splitting can solve over 80% of development problems.</p><p>For example, “Putting an elephant in the fridge takes three steps: one, open the fridge door; two, put the elephant in; three, close the fridge door.” This is a very reasonable module split. Of course, each of the three steps can be further refined. For example, opening the fridge door includes: grip the handle, pull with force in two steps; putting the elephant in includes: find a strongman, lift the elephant, place it in the fridge, release — four steps.</p><p>The core element of splitting is that it must be reasonable. For business logic, this means it should conform to our understanding of the real world. In the example above, you can’t forcibly group “pull with force, find a strongman, lift the elephant” into one method. However, this phenomenon is actually very common in real business development. Many people see a method that’s too long, know they need to split it, but just randomly grab a chunk of code and extract it. This way, the code may look a little better, but it won’t achieve the effect of reducing complexity.</p><p>Also, splitting has no end — each sub-module can be further split into finer sub-modules until the module is sufficiently easy to understand.</p><h2 id="High-Cohesion"><a href="#High-Cohesion" class="headerlink" title="High Cohesion"></a>High Cohesion</h2><p>Talking about cohesion here mainly means controlling complexity within the smallest possible scope. Using the example above, “find a strongman” is the most complex step, because there’s simply no such strongman. But the complexity here is only the internal implementation of this small module — only this one small module is complex; the higher levels or other modules shouldn’t feel this complexity.</p><p>However, in actual development, we often see some highly complex underlying structures constantly exposed across various levels and modules of the system, making it impossible for complexity to converge. As the project scale grows, the overall complexity will increase exponentially.</p><h2 id="Naming"><a href="#Naming" class="headerlink" title="Naming"></a>Naming</h2><p>This is a more detailed point. The core requirement of naming is first and foremost accuracy. For variable names, method names — when seeing the name, you should be able to roughly understand the related logic without looking at the code details.</p><p>Second, naming should have contextual consistency. For example, for a programmer, you can’t call them “coder” one moment and “programmer” the next, otherwise others will be confused when trying to understand.</p><p>Actually, that covers everything. Does it feel simpler than you expected? The truth is, as long as you do module splitting well and put each piece of code in the right place it belongs, development isn’t that complex.</p><p>Original article: <a href="https://lichuanyang.top/posts/33852/">https://lichuanyang.top/posts/33852/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/33852/">https://lichuanyang.top/en/posts/33852/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/33852/</id>
    <link href="https://lichuanyang.top/en/posts/33852/"/>
    <published>2021-01-04T10:12:34.000Z</published>
    <summary>A systematic analysis of the causes of software complexity and methods to reduce it, covering module division, eliminating duplication, and decoupling.</summary>
    <title>Reducing Software Development Complexity Through Proper Design</title>
    <updated>2026-06-27T03:53:05.194Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <content>
      <![CDATA[<h2 id="What-Is-Information-Attenuation"><a href="#What-Is-Information-Attenuation" class="headerlink" title="What Is Information Attenuation"></a>What Is Information Attenuation</h2><p>Seeing this title, you might think this is going to be about information science. It’s actually not — I just recently observed some things and felt compelled to share my thoughts. I happened to come across two statements online where I happened to be quite familiar with the facts, so I感慨ed that second-hand information is truly unreliable.</p><span id="more"></span><h2 id="Real-World-Cases"><a href="#Real-World-Cases" class="headerlink" title="Real-World Cases"></a>Real-World Cases</h2><p>The first case involves a primary school called Sanfan Affiliated Primary School in the Desheng district of Xicheng, Beijing. Because students can directly enter Sanfan Middle School — one of the top middle schools in Xicheng — this school has always been extremely popular. This year, due to insufficient classroom space, enrollment was reduced from seven classes to four, so the registration residency requirement was raised to four years. Those who hadn’t registered for four years would be reassigned. Actually, the reassignment wasn’t bad, because among the reassignment schools was Xishi Affiliated Primary School, ranked in the top five in Xicheng with ample school places, as well as high-quality direct-entry schools like Leifeng. Some people even bought Sanfan Affiliated Primary School district housing specifically to be reassigned, because Xishi Affiliated Primary School district housing is actually more expensive.</p><p>Then, in a highly upvoted comment on a WeChat official account, I saw the claim that Desheng had raised the registration residency requirement to “two or three years” this year. The commenter even used this as a basis to express intellectual contempt for buying school district housing.</p><h2 id="Three-Levels-of-Attenuation"><a href="#Three-Levels-of-Attenuation" class="headerlink" title="Three Levels of Attenuation"></a>Three Levels of Attenuation</h2><p>Combining the facts with the final statement, you can roughly guess how the information changed throughout the transmission chain. Sanfan Affiliated Primary School in Desheng gradually became just “Desheng,” the four-year residency requirement became “two or three years of residency before being reassigned,” and then further shortened to “requiring two or three years.” Meanwhile, critical information like the fact that reassignment went to Xishi Affiliated Primary School was lost entirely.</p><p>The second case: last week, COVID cases appeared in the Wangjing area, and various communities organized mass nucleic acid testing over the weekend. At one testing site, for some reason, the queue was extremely long. However, most testing sites actually had short queues — at some sites, you even had to wait a while to accumulate a group of 5 or 10 people before testing could proceed.</p><p>Then this week, I happened to hear a finance influencer talk about this incident, and the information became “tens of thousands of people in the Wangjing area standing in the cold wind queuing for nucleic acid testing.” Actually, what he said wasn’t wrong, and he had no ill intentions — he just mentioned in passing that the pandemic was getting a bit more serious again. But as you can feel, with just a few details missing, the entire impression changes completely. Reading this simplified version, you’d feel quite sorry for the people in Wangjing, but in reality, most people completed their testing easily and happily. As for the site with the long queue, you could always go to another testing site at any time.</p><p>So, looking at these two cases — though both are very small matters — I’ve completely lost trust in second-hand information. During transmission, the loss or slight alteration of key details can make the entire meaning completely different. Furthermore, when some people mention things in passing, they don’t consider it an important matter and won’t verify whether the information is accurate. But the listener’s judgment of the matter’s importance may differ significantly. If you make decisions based on such information, you can imagine the consequences.</p><h2 id="How-to-Combat-Attenuation"><a href="#How-to-Combat-Attenuation" class="headerlink" title="How to Combat Attenuation"></a>How to Combat Attenuation</h2><p>So, what counts as first-hand information? Here’s a simple list:</p><ul><li>Facts. These are things that actually happened — for example, “the temperature in location X today is Y degrees” is a definitive event, whereas “it’s very cold in location X today” is a subjective judgment.</li><li>Professional books. Books go through proofreading processes and have been tested by countless readers, so their credibility is much higher.</li><li>Public policies. I won’t elaborate on this, as they are the standard.</li><li>Data from reliable sources.</li></ul><p>In summary, I hope everyone will seek out first-hand information, avoid second-hand information, and especially pay attention to information credibility when making important decisions.</p><p>Source: <a href="https://lichuanyang.top/en/posts/54296/">https://lichuanyang.top/en/posts/54296/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/54296/</id>
    <link href="https://lichuanyang.top/en/posts/54296/"/>
    <published>2020-12-29T03:28:44.000Z</published>
    <summary>Through two real-life cases, analyzing the distortion and attenuation of information during transmission, revealing the unreliability of second-hand information.</summary>
    <title>Information Attenuation During Transmission</title>
    <updated>2026-06-27T01:14:03.205Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <category term="journal" scheme="https://lichuanyang.top/en/tags/journal/"/>
    <content>
      <![CDATA[<p>I started looking at houses around the National Day holiday this year, and made a decision by early last month. I didn’t expect to settle so quickly — I’d say destiny just had its way. Looking back at the experience, I feel it’s worth documenting, as it’s one of the biggest decisions in life.</p><span id="more"></span><h2 id="Background-and-Requirements"><a href="#Background-and-Requirements" class="headerlink" title="Background and Requirements"></a>Background and Requirements</h2><p>Actually, I had been thinking about buying school district housing since the beginning of the year when my child was born. However, my current property wouldn’t reach the five-year mark until early next year, so I couldn’t get in before the Xicheng 731 policy deadline — a small regret.</p><p>Regarding whether to buy school district housing, I strongly agree with a viewpoint I’ve seen online: buying a house is essentially buying a social circle. You’re buying neighbors of a certain type and classmates for your children. At the same price point, some people would choose a better school district with a slightly worse living condition, while others would prefer more comfortable living with a slightly lower-rated school district. After some thought, I’d rather be in the first group.</p><p>So my spouse and I established our basic principle: pursue the best school district within our budget while ensuring a basic quality of living. After calculating our budget, we aimed for around 8.5 million RMB, with a maximum of 9.5 million. A small two-bedroom unit with a decent floor plan would suffice.</p><h2 id="Area-Analysis"><a href="#Area-Analysis" class="headerlink" title="Area Analysis"></a>Area Analysis</h2><p>First, I did an initial round of screening. Most areas in Haidian were too far from both our workplaces, so they were eliminated. Financial Street had too much price pressure. So I narrowed the scope to the Yuetan and Desheng areas in Xicheng first to look at the housing situation.</p><p>During the National Day holiday, I went to Yuetan to look at houses, mainly in the neighborhoods served by Yumin Primary School — Baiyunguan, Bailu Road, Qinan, Zhenwumiao, and so on. Honestly, the gap between expectations and reality was huge. My first impression was that it was dirty, messy, and run-down — I almost gave up on this area.</p><p>So for the next two weeks, the focus shifted primarily to Desheng. I made numerous trips to Desheng and looked at almost every neighborhood there. Overall, the first impression of the neighborhood environments was definitely better than what I’d seen in Yuetan. At one point, I was almost ready to make a decision, but during a second visit, I experienced Desheng’s narrow alleys again and decided I couldn’t accept that, so I gave up. This meant giving up on most Desheng neighborhoods, leaving only Putian Courtyard (served by Yuxiang Primary School), a few buildings in Yuzhong Dongli (served by Sanfan Affiliated Primary School), and Liupukang Districts 1 and 2 (served by Xishi Affiliated Primary School) as backup options. However, all three had significant drawbacks — Putian Courtyard and Liupukang had very old buildings, and Sanfan Affiliated Primary School’s district had too many properties with high transaction volumes, making the risk of reassignment after the 731 policy too high.</p><p>After considering these points, I felt there would always be an underlying concern. Plus, Desheng’s school district premium was too heavy — unlike Yuetan, which had the added advantage of its location, the risk of being stuck with an overpriced property was also significant. So while waiting for new listings in these neighborhoods, I also added Dongcheng and Chaoyang to the consideration. Mainly the Anjiao and Hepingli areas in Dongcheng, and Chen Jinglun Zhujiang Dijing and Chaoyang Foreign Language School areas in Chaoyang. Before visiting in person, I first looked online. Among existing listings, Zhujiang Dijing units I could afford were all north-facing, so that was out for now. The other options didn’t show any major issues overall, but the living quality didn’t feel noticeably better than Xicheng, so they didn’t seem right.</p><p>So these remained as backup options — only to be considered if a very satisfactory property appeared. Then I went back to Yuetan to look at houses a few more times. Surprisingly, these visits felt significantly better than the first time. I’m not sure if it was because I’d gotten used to it or because of the agents — maybe the first time they showed me all the worst properties in Yuetan?</p><p>During these subsequent visits, I became quite familiar with the situation of each neighborhood in Yuetan. But there really wasn’t a property that satisfied all criteria. So I told the agents at each location and decided to wait for new listings before looking again.</p><h2 id="The-Final-Decision"><a href="#The-Final-Decision" class="headerlink" title="The Final Decision"></a>The Final Decision</h2><p>Fortunately, at the end of October, a property appeared that was excellent in every way — nice neighborhood environment, built in 2000 (the newest in the surrounding area), close to the metro, high floor plan utilization, and acceptable orientation and lighting. Of course, except for the price. But the agent still recommended we meet with the landlord to talk. We talked from 4 PM until 10 PM that day. The owner was indeed a very straightforward and kind person, but the price gap between us was significant. Moreover, the property belonged to his parents, so he could only keep calling to persuade them. In the end, we couldn’t reach an agreement that night.</p><p>I have to say, the green agency (Homelink) was truly impressive, and the owner was great too. After we went home that night, the agent continued working on the owner, and the owner continued persuading his parents. Finally, close to midnight, he agreed to our offer. Then at midnight, we rushed back to sign the contract.</p><p>Through this process, I pretty much figured out how real estate agents work — haha. The strategy is to first get both parties to meet and talk, then wear them down. Once both sides are mentally exhausted, the price becomes easier to negotiate. ^_^</p><p>Looking back on the entire month-long journey now, I can describe it calmly. Actually, during that period, the psychological pressure was immense. On one hand, choosing a house was extremely difficult. At the same time, I was torn — afraid of buying at a peak, and afraid the multi-school policy would be too strictly enforced. Now that everything is settled, my mindset is actually okay. After all, I’ve done everything possible to minimize risks. All I can do is my best and leave the rest to fate. As long as two things don’t happen simultaneously — the property value stagnating and being reassigned to a low-quality school — that’s acceptable for me.</p><p>Of course, I’m still hoping for a good outcome ^_^.</p><p>Source: <a href="https://lichuanyang.top/en/posts/5236/">https://lichuanyang.top/en/posts/5236/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/5236/</id>
    <link href="https://lichuanyang.top/en/posts/5236/"/>
    <published>2020-12-12T09:12:40.000Z</published>
    <summary>A record of house hunting experience in Beijing Xicheng school district in 2020, sharing the thought process behind property selection and how school district policies affect purchasing decisions.</summary>
    <title>House Hunting Notes — 2020 Beijing School District</title>
    <updated>2026-06-27T01:14:03.225Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Book Notes" scheme="https://lichuanyang.top/en/categories/Book-Notes/"/>
    <category term="investing" scheme="https://lichuanyang.top/en/tags/investing/"/>
    <category term="personal-growth" scheme="https://lichuanyang.top/en/tags/personal-growth/"/>
    <category term="book-reflections" scheme="https://lichuanyang.top/en/tags/book-reflections/"/>
    <content>
      <![CDATA[<p>“Civilization, Modernization, Value Investing, and China” is a book by the renowned value investor Mr. Lu Li. It discusses the development of civilization, the emergence of modernization, and value investing, and explores these topics in the context of China’s actual situation. I personally feel this is an extremely valuable book worth reading repeatedly. It is of great help in understanding the logic of how this world operates and in building correct investment philosophy.</p><span id="more"></span><p>As the title suggests, this book actually covers two relatively independent topics: civilization and modernization, and value investing. It also discusses each topic’s relationship with China.</p><h2 id="Civilization-and-Modernization"><a href="#Civilization-and-Modernization" class="headerlink" title="Civilization and Modernization"></a>Civilization and Modernization</h2><h2 id="The-Limitations-of-Agricultural-Civilization"><a href="#The-Limitations-of-Agricultural-Civilization" class="headerlink" title="The Limitations of Agricultural Civilization"></a>The Limitations of Agricultural Civilization</h2><p>The author first devoted considerable space to describing the entire process of civilization’s emergence and evolution. Particularly精彩的 is the discussion of the ceiling of agricultural civilization. Agricultural civilization’s output essentially comes from photosynthesis. Plants grow through photosynthesis, and raising livestock also requires consuming plants. The upper limit of energy produced by photosynthesis is constrained by land area and per-unit land output — both of which have very clear upper limits, so natural resources also have upper limits. Limited natural resources and nearly unlimited population growth mean that the population can ultimately only be reduced through non-natural disasters.</p><h2 id="The-Emergence-and-Essence-of-Modernization"><a href="#The-Emergence-and-Essence-of-Modernization" class="headerlink" title="The Emergence and Essence of Modernization"></a>The Emergence and Essence of Modernization</h2><p>The emergence of modernization originated from the Atlantic free market economy brought by the New World and the simultaneous Scientific Revolution. 1776 was an important year when three events occurred that were crucial to modernization’s development. First, Adam Smith published “The Wealth of Nations,” whose core topic was the nature of the Atlantic economy — free trade. Second, the Declaration of Independence was issued, giving birth to a new regime deeply influenced by Adam Smith’s theories, which would greatly help the development of free markets. Third, the steam engine was created, dramatically increasing productivity. From then on, the combination of free markets and technology released astonishing power.</p><p>The free market itself is a mechanism that can continuously self-evolve, self-improve, and self-perfect. The intervention of modern technology has made this process incredibly rapid. Thus, among competing markets, the largest market will eventually become the only market.</p><p>Any organization that leaves this market will continuously fall behind, just as the Soviet Union did in its time.</p><p>Many of China’s initiatives in recent years, such as the Belt and Road Initiative and RCEP, are aimed at increasing the scale of the free market it can influence. We should thank Trump for continuously withdrawing from groups and leaving this largest free market on his own.</p><h2 id="Modernization-and-China"><a href="#Modernization-and-China" class="headerlink" title="Modernization and China"></a>Modernization and China</h2><p>So, could modernization have been born in China? The answer is no. Because in the process of modernization’s emergence, private capital has always played a crucial role. Since the Qin and Han dynasties, China has had a very strong central government, making it difficult for private capital to rise. Additionally, the original intent of discovering the New World was to find China. As the center of civilization at that time, China had no motivation to seek out more prosperous places.</p><p>It can be said that the emergence of modernization originally stemmed from a series of very coincidental factors. The originally barbarous Anglo-Saxons won this lottery, allowing them to get ahead through a shortcut. But history will ultimately return to where it should be. So there’s no need to regret why modernization didn’t originate in China.</p><p>Over the past few decades, China’s rapid development has actually been following the path others have taken. In this situation, a strong government can play a very important role. But as we gradually catch up with others’ progress and need to explore the next direction, the free market will gradually demonstrate its advantages.</p><p>Culturally, in China, restoring traditional culture to its proper position is really the only option. Culture has deep historical and geographical origins, having long since penetrated into one’s bones. As the economy develops, people’s spiritual needs will continue to improve, and a Chinese-style Renaissance may well emerge, allowing people to rediscover the most essential parts of Chinese culture.</p><p>One day, most Chinese people will realize that the Twenty-Four Histories, Tang and Song poetry, the Four Great Classical Novels, the Analerta, the Tao Te Ching, and the Art of War — these ancient cultural heritages — are far superior to a culture that says “thank the Native Americans for being killed by us.”</p><h2 id="Value-Investing"><a href="#Value-Investing" class="headerlink" title="Value Investing"></a>Value Investing</h2><h2 id="The-Current-State-of-the-Investment-Industry"><a href="#The-Current-State-of-the-Investment-Industry" class="headerlink" title="The Current State of the Investment Industry"></a>The Current State of the Investment Industry</h2><p>When it comes to investing, we must look at the current state of the entire industry. The biggest problem in the current investment industry is, of course, the inability to evaluate whether a product is good or bad. Investing is a long-term process, and short-term fluctuations have virtually no reference value. The current profit model of funds means that fund managers with poor performance can still collect large fees.</p><p>The author mentions an important concept — fiduciary responsibility. Only when most people in the industry embrace this concept can the industry develop in a positive direction.</p><h2 id="Historical-Experience-of-Investing"><a href="#Historical-Experience-of-Investing" class="headerlink" title="Historical Experience of Investing"></a>Historical Experience of Investing</h2><p>Over the long term, stocks’ returns far exceed those of any other type of asset by several orders of magnitude. In China, this is no exception. There are two reasons: first, inflation, and second, economic growth. Stocks can in fact largely reflect GDP growth.</p><h2 id="The-Philosophy-of-Value-Investing"><a href="#The-Philosophy-of-Value-Investing" class="headerlink" title="The Philosophy of Value Investing"></a>The Philosophy of Value Investing</h2><p>The basic principles of value investing are only four.</p><h3 id="Stocks-Represent-Ownership-of-Companies"><a href="#Stocks-Represent-Ownership-of-Companies" class="headerlink" title="Stocks Represent Ownership of Companies"></a>Stocks Represent Ownership of Companies</h3><p>Investment returns come from company development. The reason you can earn returns is because you’ve contributed to the company’s development. This is not a zero-sum game. Profiting from betting on market fluctuations is speculation — a zero-sum or even negative-sum game no different from gambling. Therefore, value investing is the right path, the grand path. We create value for society and then receive returns — that’s all there is to it. We don’t hope to obtain returns through deception or exploitation of others.</p><h3 id="Understanding-the-Market"><a href="#Understanding-the-Market" class="headerlink" title="Understanding the Market"></a>Understanding the Market</h3><p>The market is just an institution that provides you with services. You can get buying opportunities here, and you can sell when you need money. Other information the market reveals, such as the short-term fluctuations of a stock, is purely noise.</p><h3 id="Margin-of-Safety"><a href="#Margin-of-Safety" class="headerlink" title="Margin of Safety"></a>Margin of Safety</h3><p>Investment is essentially a prediction of the future, and predictions are inevitably not 100% accurate. Therefore, increasing the margin of safety is particularly important.</p><h3 id="Building-a-Circle-of-Competence"><a href="#Building-a-Circle-of-Competence" class="headerlink" title="Building a Circle of Competence"></a>Building a Circle of Competence</h3><p>A circle of competence can be developed through long-term effort and training. Before building a circle of competence, there’s a prerequisite: we need to have awareness of our own circle of competence and be able to identify the boundaries of our abilities. This requires an honest attitude toward knowledge. When we hold a view, we need to find the smartest person who disagrees with us and be able to refute their viewpoint — only then can we claim to truly hold that view.</p><p>As for how to build a circle of competence, it’s different for everyone, depending on each person’s usual learning methods and abilities. However, Mr. Lu Li has given us some suggestions: 1. Look at businesses from an owner’s perspective; 2. Accumulate gradually; 3. Let interest and opportunity guide your research; 4. Practice more — choose a company and research it thoroughly with an investment mindset.</p><h2 id="Who-Is-Suited-for-Value-Investing"><a href="#Who-Is-Suited-for-Value-Investing" class="headerlink" title="Who Is Suited for Value Investing"></a>Who Is Suited for Value Investing</h2><p>Finally, let me discuss some requirements that value investing places on one’s character.</p><ul><li>Independence — one needs to value one’s own internal judgment standards more</li><li>Relatively objective — less influenced by emotions</li><li>Patience and decisiveness. These two requirements may seem somewhat contradictory, but for a value investor, one needs enough patience to wait years without acting, yet once the right moment arrives, be able to act quickly and decisively</li><li>Must have an interest in business</li></ul><p>Source: <a href="https://lichuanyang.top/en/posts/44866/">https://lichuanyang.top/en/posts/44866/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/44866/</id>
    <link href="https://lichuanyang.top/en/posts/44866/"/>
    <published>2020-12-12T07:24:34.000Z</published>
    <summary>Reflections on Lu Li's 'Civilization, Modernization, Value Investing, and China' — a systematic cognitive upgrade from civilization development to value investing.</summary>
    <title>Book Reflections: Civilization, Modernization, Value Investing, and China</title>
    <updated>2026-06-27T03:51:55.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Book Notes" scheme="https://lichuanyang.top/en/categories/Book-Notes/"/>
    <category term="personal-growth" scheme="https://lichuanyang.top/en/tags/personal-growth/"/>
    <category term="book-reflections" scheme="https://lichuanyang.top/en/tags/book-reflections/"/>
    <category term="education" scheme="https://lichuanyang.top/en/tags/education/"/>
    <content>
      <![CDATA[<p>The book I’m discussing today, “I Walked on the Edge of Breakdown: Yu Minhong’s Personal Account of New Oriental’s Entrepreneurial Development,” is an autobiographical work by Mr. Yu Minhong, recounting his many experiences and psychological journey since founding New Oriental. Today, I’ll summarize the content of this book and share my personal reflections.</p><span id="more"></span><p>The book begins before Yu Minhong founded New Oriental. Yu Minhong was originally a teacher at Peking University, and he left the university after conflicts with the school. At the time, there was a trend of studying abroad, and Yu Minhong initially planned to train others for the GRE exam to save money for studying abroad. But through a series of coincidences, New Oriental grew larger and larger.</p><p>In simple terms, New Oriental’s development went through the following stages:</p><ol><li>The small workshop stage where Yu Minhong handled almost everything himself</li><li>Obtaining a school operating license and gradually moving toward formalization;</li><li>The brand began to spread explosively</li><li>Old classmates like Xu Xiaoping and Wang Qiang joined, and Yu Minhong began to have many strong partners</li><li>Family members departed, Western management knowledge was adopted, and New Oriental began to establish clear systems</li><li>The company was established, along with continuous reforms to the organizational structure</li><li>External investment came in, leading to the IPO</li><li>Old shareholders exited, the new generation rose; new business areas continued to develop, up to the New Oriental of today</li></ol><p>Through this book, we see New Oriental’s journey from a small training center to a leading domestic educational institution, and there are many insights to be gained.</p><h2 id="About-Growth"><a href="#About-Growth" class="headerlink" title="About Growth"></a>About Growth</h2><p>Reading the biographies of all successful people, you’ll find they share a common trait: they don’t limit themselves, they don’t say “I can only do these things, I can’t do those things.” Yu Minhong is no exception. New Oriental originated from the fact that Yu Minhong’s English was indeed very good, and he caught the wave of studying abroad. But looking back today, we can see that Yu Minhong’s English ability was only a small part of New Oriental’s entire development process.</p><p>From the moment Yu Minhong started his training classes, he had to prepare lessons for various subjects and think about how to market. As the company grew, he had to continuously learn about company management, acquire financial knowledge, and constantly solve various new problems.</p><p>Yu Minhong also often says in the book that in the beginning he didn’t understand company management or finance, and usually only thought about how to solve problems after they arose. Solving these problems required continuous growth of one’s own capabilities — either through self-learning or bringing in external expertise — all of which require constantly expanding one’s circle of competence.</p><p>In the course of career development, there will inevitably be many unfamiliar areas at the boundaries of our work. If we maintain an open attitude toward these areas, we can naturally expand our capabilities. When our capabilities expand, we’ll have more opportunities, and within those opportunities, we can encounter new areas. This creates a positive cycle that promotes both career development and the expansion of our circle of competence.</p><p>Of course, in the process of growth, luck is indispensable. The experiences of the successful people we see all carry survivorship bias. New Oriental’s development process does have several instances that can be attributed to good luck. But we can imagine: if we removed those lucky factors, Yu Minhong’s development path might have been saving money to go abroad, then most likely finding a good job; or he might have continued the training business. Even without favorable policies, as long as he kept expanding his circle of competence, I think it would have been enough for him to live quite well.</p><p>In short, great success depends on fate, but if we can always maintain our own growth, ensuring a comfortable middle-class life is still achievable.</p><h2 id="About-Education"><a href="#About-Education" class="headerlink" title="About Education"></a>About Education</h2><p>New Oriental is, after all, an education company. I happen to work at an online education company, so I paid particular attention to the discussion of the education business. After finishing this book, the biggest insight about education is that regardless of which users you target, which subject you teach, or what format you use, the foundation of everything can only be the quality of teaching itself. Only teaching itself can sustain an institution over the long term. Perhaps you can attract a large number of students in the short term through massive marketing, or use sales tactics to convince users to enroll in more expensive courses — there are many ways to achieve high returns in the short term. But no one is foolish, and users will soon vote with their feet.</p><h2 id="About-Rules"><a href="#About-Rules" class="headerlink" title="About Rules"></a>About Rules</h2><p>For an organization, reasonable rules are essential. Regarding rules, the two most important points are: first, they must exist, and second, they must be reasonable.</p><p>New Oriental’s transformation from a family business to a formal enterprise was a process of rules going from nothing to something. Subsequently, whether it was the establishment of the board of directors, the development of branch schools, or determining the assessment mechanisms for branches, it was a process of rules continuously developing. When rules are well-designed, the company can continue to grow and develop, achieving a win-win situation for the enterprise and employees. When rules are poorly designed, the company will inevitably head toward breakdown. The key to good rules is enabling employees’ personal interests to be transformed into company interests. People are highly adaptable — if you evaluate branches based on their revenue, then branches will find all sorts of ways to increase revenue, but in the process, they’ll drain the company’s reputation. Under good rules, employees are still pursuing their own interests, but in doing so, they naturally contribute to the company’s development.</p><p>In addition, there are many other scattered insights, such as various business models in New Oriental’s development process — like the large class model, like early marketing methods; and the importance of standardization for a company to grow big. In short, although this book is just an autobiography, it’s packed with valuable insights and worth reading several times carefully.</p><h2 id=""><a href="#" class="headerlink" title=""></a></h2><p>Source: <a href="https://lichuanyang.top/en/posts/6471/">https://lichuanyang.top/en/posts/6471/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/6471/</id>
    <link href="https://lichuanyang.top/en/posts/6471/"/>
    <published>2020-10-31T02:35:22.000Z</published>
    <summary>Book notes on Yu Minhong's 'I Walked on the Edge of Breakdown', reviewing New Oriental's entrepreneurial journey and key decisions from founding to growth.</summary>
    <title>Book Notes: I Walked on the Edge of Breakdown</title>
    <updated>2026-06-27T03:53:05.198Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Distributed Systems Patterns Series" scheme="https://lichuanyang.top/en/categories/Distributed-Systems-Patterns-Series/"/>
    <category term="distributed-systems" scheme="https://lichuanyang.top/en/tags/distributed-systems/"/>
    <category term="distributed-system-design" scheme="https://lichuanyang.top/en/tags/distributed-system-design/"/>
    <content>
      <![CDATA[<p>(I recently reorganized this article. Welcome to read the latest version: <a href="https://lichuanyang.top/posts/45718/">https://lichuanyang.top/posts/45718/</a>)</p><p>This article is translated from <a href="https://martinfowler.com/articles/patterns-of-distributed-systems/">https://martinfowler.com/articles/patterns-of-distributed-systems/</a>. The original author summarizes various distributed systems used in enterprise architectures, extracting common “patterns.” As the first article in this series, it introduces the characteristics of distributed systems and some common issues. I highly recommend reading this article and the original English version — they will be very helpful for understanding distributed system design and distributed architecture philosophy.</p><span id="more"></span><h2 id="What-This-Is-About"><a href="#What-This-Is-About" class="headerlink" title="What This Is About"></a>What This Is About</h2><p>Distributed systems bring special challenges to program design. They typically require us to have multiple data replicas that need to stay in sync. However, we cannot expect all nodes to work reliably all the time, and network delays can easily lead to inconsistencies. Despite this, many organizations rely on a range of core distributed systems for data storage, messaging, system management, and computation. These systems face many common problems, which can also be solved with similar approaches. This article defines these solutions as patterns, through which we can build a better understanding of how to understand, communicate about, and teach distributed systems.</p><p>Over the past few months, I have been conducting workshops on distributed systems at ThoughtWorks. One of the main challenges is how to map the theory of distributed systems to open-source projects like Kafka and Cassandra, while keeping the discussion general enough to cover a broader range of solutions. The concept of patterns provides a nice way out.</p><p>The “pattern” structure essentially allows us to focus on a specific problem and clearly articulate why a particular solution is needed. The description of the solution then allows us to present a code structure that is concrete enough to represent an actual solution, yet general enough to cover various variations. Patterns also allow us to chain various patterns together to build a complete system. In this way, we have a sufficient vocabulary to discuss distributed system implementations.</p><p>What follows is the first set of patterns observed in mainstream open-source distributed systems. I hope this set of patterns is useful for all developers.</p><h2 id="Distributed-Systems-—-Implementation-Perspective"><a href="#Distributed-Systems-—-Implementation-Perspective" class="headerlink" title="Distributed Systems — Implementation Perspective"></a>Distributed Systems — Implementation Perspective</h2><p>Current enterprise architectures contain many naturally distributed platforms and frameworks. If we sample the platforms and frameworks in a typical architecture today, we see the following:</p><table><thead><tr><th align="left">Type of platform&#x2F;framework</th><th align="left">Example</th></tr></thead><tbody><tr><td align="left">Databases</td><td align="left">Cassandra, HBase, Riak</td></tr><tr><td align="left">Message Brokers</td><td align="left">Kafka, Pulsar</td></tr><tr><td align="left">Infrastructure</td><td align="left">Kubernetes, Mesos, Zookeeper, etcd, Consul</td></tr><tr><td align="left">In Memory Data&#x2F;Compute Grids</td><td align="left">Hazlecast, Pivotal Gemfire</td></tr><tr><td align="left">Stateful Microservices</td><td align="left">Akka Actors, Axon</td></tr><tr><td align="left">File Systems</td><td align="left">HDFS, Ceph</td></tr></tbody></table><p>These systems are all naturally distributed. Regarding what makes a system “distributed,” there are two aspects:</p><ul><li>Running on multiple servers. The number of servers in a cluster can vary greatly, from three to thousands.</li><li>Needing to handle data, which makes them inherently “stateful” systems.</li></ul><p>When multiple servers are involved in storing data, there are several ways things can go wrong. All the systems listed above need to solve these problems. There are recurring solution patterns in how these systems are implemented. Understanding these solutions in a more general form helps in understanding the wide range of implementations and serves as good guidance when building new systems.</p><p>Patterns, proposed by Christopher Alexander, are widely accepted in the software community as a way to document design architecture in software system design. Patterns provide a structured way to solve problems through repeated observation and verification. An interesting way to use patterns is to chain multiple patterns together in the form of a pattern sequence or pattern language, which provides some guidance for implementing “the whole” or a complete system. Viewing distributed systems as a series of patterns is an effective way to gain deeper insight into their implementations.</p><h2 id="Problems-and-Their-Recurring-Solutions"><a href="#Problems-and-Their-Recurring-Solutions" class="headerlink" title="Problems and Their Recurring Solutions"></a>Problems and Their Recurring Solutions</h2><p>When data is stored across multiple servers, there are several ways things can go wrong.</p><h2 id="Process-Crashes"><a href="#Process-Crashes" class="headerlink" title="Process Crashes"></a>Process Crashes</h2><p>A process can crash at any time due to hardware or software factors. There are several ways a process can crash:</p><ul><li>System administrators perform routine system maintenance, and the process is shut down.</li><li>An I&#x2F;O operation fails due to insufficient disk space, and the exception is not handled correctly, causing the process to be killed.</li><li>In cloud environments, things can be more complex; various unrelated reasons can cause system downtime.</li></ul><p>Most importantly, if a process is responsible for storing data, it must be designed to provide durability guarantees for data stored on the server. Even if the process crashes suddenly, it should retain all data for which it has already acknowledged receipt from the client. Depending on the access pattern, different storage engines have different storage structures, ranging from simple hash tables to complex graph structures. Flushing data to disk is one of the most time-consuming operations, so it’s not possible to flush every insert or update to disk immediately. Therefore, most databases have in-memory storage structures, with data periodically flushed to disk. If the process crashes suddenly, all of this data may be lost.</p><p>A technique called Write-Ahead Log is used to solve this problem. The server stores each state change as a command in an append-only file on disk. Append operations are typically very fast, so they can be completed without impacting performance. Sequential writes to a single log file store each update. On server startup, the log can be replayed to rebuild the in-memory state.</p><p>This provides durability guarantees. Even if the server crashes suddenly and restarts, data is not lost. However, clients will not be able to read or store any data until the server recovers. Therefore, if a server fails, we lack availability.</p><p>An obvious solution is to store data on multiple servers. Thus, we can replicate the write-ahead log across multiple servers.</p><p>When multiple servers are involved, there are more failure scenarios to consider.</p><h2 id="Network-Latency"><a href="#Network-Latency" class="headerlink" title="Network Latency"></a>Network Latency</h2><p>In the TCP&#x2F;IP protocol stack, there is no upper bound on the delay caused by transmitting messages over the network. It varies based on network load. For example, a 1 Gbps network connection can be overwhelmed by large batch jobs that run periodically, filling network buffers and potentially causing some messages to reach the server with indefinite delays.</p><p>In a typical data center, servers are packed into racks, and multiple racks are connected through rack switches. There may be a tree of switches connecting one part of the data center to another. In some cases, a group of servers can communicate with each other but is disconnected from another group of servers. This situation is called a network partition. One of the fundamental problems with servers communicating over the network is determining when a particular server has failed.</p><p>There are two problems to solve:</p><ul><li>A particular server cannot know indefinitely whether another server has gone down.</li><li>There should not be two sets of servers, each believing the other has failed, and therefore continuing to serve different clients. This is called split brain.</li></ul><p>To solve the first problem, each server periodically sends heartbeat messages to other servers. If no heartbeat is received, the corresponding server is considered crashed. Heartbeat intervals are short enough to ensure that server failure is detected quickly. In the worst case, a server may still be running but is considered down by the cluster as a whole, and continues to operate. This heartbeat mechanism ensures that the service provided to clients is not interrupted.</p><p>The second problem is split brain. In a split-brain situation, if two groups of servers independently accept updates, different clients can read and write different data. Once the split-brain issue is resolved, it is impossible to automatically reconcile the conflicts.</p><p>To solve the split-brain problem, we must ensure that two disconnected groups of servers cannot continue to operate independently. To ensure this, a server only considers an action successful if a majority of servers can acknowledge it. If the servers cannot elect a majority, they will be unable to serve, and some client groups may not receive the service. However, the servers in the cluster will always be in a consistent state. The number of servers forming a majority is called a Quorum. How is the Quorum determined? It is based on the number of failures the cluster can tolerate. For example, if we have a five-node cluster, we need a Quorum of three. Typically, if we want to tolerate f failures, we need a cluster size of 2f + 1.</p><p>A Quorum ensures we have enough data replicas to withstand some server failures. However, this alone is not enough to provide strong consistency guarantees to clients. Suppose a client initiates a write on the Quorum, but the write succeeds on only one server. Other servers in the Quorum still have old values. When the client reads from the Quorum, it may get the latest value if the server with the latest value is available. However, if the server with the latest value is unavailable when the client starts reading, it could get the old value. To avoid this, someone needs to track whether the Quorum agrees on a particular operation and only send values to clients that are guaranteed to be available on all servers. This is where the Master-Slave pattern is used. One server is elected as the master, and the other servers act as slaves. The master controls and coordinates replication to the slaves. The master needs to determine which changes should be visible to clients. A High-Water Mark is used to track entries in the write-ahead log that have been successfully replicated to enough slaves. Clients can see all entries before the high-water mark. The master also sends the high-water mark to slaves. Therefore, if the master fails and one of the slaves becomes the new master, clients will not see inconsistencies.</p><h2 id="Process-Pauses"><a href="#Process-Pauses" class="headerlink" title="Process Pauses"></a>Process Pauses</h2><p>But that’s not all. Even with Quorums and Master and Slave, there is still a tricky problem to solve. The master process can pause at any time. There are many reasons for process pauses. In languages that support garbage collection, there may be long garbage collection pauses. A master with long garbage collection pauses may disconnect from the slaves and resume sending messages after the pause ends. Meanwhile, if the slaves haven’t received any heartbeat from the master, they may have elected a new master and accepted client updates. If the old master’s requests are processed as-is, they may overwrite some updates. Therefore, we need a mechanism to detect requests from a stale master. A Generation Clock is used to tag and detect requests from an old master. A generation is a monotonically increasing number.</p><h2 id="Clocks-Not-in-Sync-and-Message-Ordering"><a href="#Clocks-Not-in-Sync-and-Message-Ordering" class="headerlink" title="Clocks Not in Sync and Message Ordering"></a>Clocks Not in Sync and Message Ordering</h2><p>The problem of detecting old master messages from newer ones is how to guarantee message ordering. It may seem that we can use system time to order a set of messages, but in practice, we cannot. The main reason is that we cannot guarantee that system clocks across servers are synchronized. The clock on a computer during the day is managed by a quartz crystal and measures time based on the crystal’s oscillation.</p><p>This mechanism is error-prone because the crystal can oscillate faster or slower, so different servers may have significantly different times. The clocks on a set of servers are synchronized through a service called NTP. This service periodically checks a set of global time servers and adjusts the computer clock accordingly.</p><p>Since this happens through network communication, and network latency can vary as described in the previous sections, clock synchronization may be delayed due to network issues. This can cause server clocks to drift from each other and even go backward after NTP synchronization. Due to these issues with computer clocks, wall-clock time is generally not used for ordering events. Instead, a simple technique called Lamport’s timestamp is used. The Generation Clock is one example of this.</p><h2 id="Putting-It-All-Together-—-A-Distributed-System-Example"><a href="#Putting-It-All-Together-—-A-Distributed-System-Example" class="headerlink" title="Putting It All Together — A Distributed System Example"></a>Putting It All Together — A Distributed System Example</h2><p>We can see how understanding these patterns from the ground up helps us build a complete system. We’ll use Consensus as an example. Distributed consensus is a specific implementation of distributed systems that provides the strongest consistency guarantees. Popular examples in enterprise architectures include Zookeeper, etcd, and Consul. They implement consensus algorithms such as ZAB and Raft to provide replication and strong consistency. There are other popular algorithms that implement consensus: Paxos used in Google’s Chubby for the locking service, and consider stamp replication and <a href="https://www.cs.cornell.edu/ken/History.pdf">virtual synchrony</a>. In very simple terms, consensus means that among a set of servers, they agree on what data is stored, the order in which it is stored, and when that data becomes visible to clients.</p><h2 id="Pattern-List-for-Implementing-Consensus"><a href="#Pattern-List-for-Implementing-Consensus" class="headerlink" title="Pattern List for Implementing Consensus"></a>Pattern List for Implementing Consensus</h2><p>The implementation of consensus uses state machine replication for fault tolerance. In state machine replication, a storage service, such as a key-value store, is replicated across all servers, and user inputs are executed on each server in the same order. The key implementation technique used for this purpose is replicating the Write-Ahead Log across all servers to create a “Replicated WAL.”</p><p>We can combine these patterns to implement a Replicated WAL as follows:</p><p>To provide durability guarantees, the Write-Ahead Log is used. The Segmented Log divides the write-ahead log into multiple segments. This helps the Low-Water Mark with log cleanup. Fault tolerance is provided by replicating the write-ahead log across multiple servers. Replication between servers is managed using the Master-Slave pattern. The Quorum is used when updating the High-Water Mark to determine which values are visible to clients. The Single Update Queue ensures all requests are processed in strict order. The Single Socket Channel is used to send master requests to slaves, maintaining event ordering. To optimize throughput and latency on the Single Socket Channel, the Request Pipeline is used. The slave determines master availability through Heartbeats. If the master is temporarily disconnected from the cluster due to a network partition, the Generation Clock can be used to detect it.</p><p><img src="/en/img/paterns.jpg" alt="patterns"></p><p>By understanding these problems and their common solutions in this way, we can understand how to build complex systems.</p><h2 id="Next-Steps"><a href="#Next-Steps" class="headerlink" title="Next Steps"></a>Next Steps</h2><p>Distributed systems are a vast topic. The set of patterns covered here is just a small portion, spanning different categories to show how the pattern approach helps in understanding and designing distributed systems. I will continue to add the following topics to this collection:</p><ul><li>Group Membership and Failure Detection</li><li>Partitioning</li><li>Replication and Consistency</li><li>Storage</li><li>Processing</li></ul><p>Original article: <a href="https://lichuanyang.top/posts/3914/">https://lichuanyang.top/posts/3914/</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/3914/">https://lichuanyang.top/en/posts/3914/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/3914/</id>
    <link href="https://lichuanyang.top/en/posts/3914/"/>
    <published>2020-08-10T12:33:49.000Z</published>
    <summary>Translation of Martin Fowler's article, summarizing common patterns of distributed systems in enterprise architectures, covering consensus, replication, partitioning and other core topics.</summary>
    <title>[Translation] [On Distributed Architecture and System Design] Patterns of Distributed Systems - Overview</title>
    <updated>2026-06-27T03:48:08.537Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Redis Series" scheme="https://lichuanyang.top/en/categories/Redis-Series/"/>
    <category term="redis" scheme="https://lichuanyang.top/en/tags/redis/"/>
    <content>
      <![CDATA[<p><a href="https://lichuanyang.top/posts/22179/">In the previous article</a>, we introduced the low-level data structures defined in Redis. Next, in this article, we will combine these with the objects provided by Redis to see how these data structures are used in practice.</p><span id="more"></span><p>Redis primarily includes the following objects: strings, lists, hashes, sets, sorted sets, HyperLogLog, GEO, etc. This article will mainly cover how these objects are implemented. For their commands, you can refer to the <a href="https://redis.io/commands">official Redis documentation</a>, which provides detailed explanations for each command.</p><p>Before diving into specific objects, let’s first take a look at Redis’s definition for the object itself.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">redisObject</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> type:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span> encoding:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span> lru:LRU_BITS; <span class="comment">/* LRU time (relative to global lru_clock) or</span></span><br><span class="line"><span class="comment">                            * LFU data (least significant 8 bits frequency</span></span><br><span class="line"><span class="comment">                            * and most significant 16 bits access time). */</span></span><br><span class="line">    <span class="type">int</span> refcount;</span><br><span class="line">    <span class="type">void</span> *ptr;</span><br><span class="line">&#125; robj;</span><br></pre></td></tr></table></figure><p>In a <code>redisObject</code>, the <code>ptr</code> field contains the actual data content. The storage here leverages the low-level Redis data structures we discussed earlier. As you can see, <code>ptr</code> is referenced within <code>redisObject</code> via a pointer, meaning the <code>redisObject</code> itself and the actual data may not be contiguous in memory.</p><p>Using the command <code>OBJECT ENCODING KEY</code>, you can inspect the underlying data structure actually used by a key.</p><p>Now, let’s dive into each object type.</p><h2 id="Strings"><a href="#Strings" class="headerlink" title="Strings"></a>Strings</h2><p>Strings are arguably the most fundamental object type in Redis. When you <code>SET</code> a regular key-value pair, you’re using a string object. String objects use one of three underlying encoding types depending on the situation: <code>int</code>, <code>embstr</code>, or <code>raw</code>. The <code>int</code> encoding is straightforward — when the string value is a number, it is stored as an integer. Both <code>embstr</code> and <code>raw</code> use a Simple Dynamic String (SDS) as the underlying structure; the difference is that with <code>embstr</code>, the <code>redisObject</code> and the SDS data occupy contiguous memory. <code>embstr</code> is used when the string is ≤ 39 bytes (using Redis’s default configuration unless otherwise noted), and <code>raw</code> is used for strings larger than 39 bytes.</p><h2 id="Lists"><a href="#Lists" class="headerlink" title="Lists"></a>Lists</h2><p>A list in Redis is a doubly linked queue that provides push&#x2F;pop operations from both the left and right sides, as well as range operations.</p><p>The underlying implementation of a list can be either a ziplist or a linked list. In the previous article, we introduced ziplist as an extremely efficient data structure for small datasets. So it’s natural that small lists are implemented with ziplist, while larger lists use linked lists. Redis determines whether a list is “small” based on two criteria: the maximum length of each element in the list (configured by <code>list-max-ziplist-value</code>), and the number of elements in the list (configured by <code>list-max-ziplist-entries</code>).</p><p>The practical applications of lists leverage the characteristics of a doubly linked queue. For example, WeChat Moments timeline could be implemented using a Redis list. Sometimes, Redis lists are also used as message queues.</p><h2 id="Hashes"><a href="#Hashes" class="headerlink" title="Hashes"></a>Hashes</h2><p>What we commonly refer to as a map. Similar to list objects, hash objects choose between two underlying structures based on dataset size: a ziplist or a hash table (based on a dictionary internally).</p><p>This leads to a useful tip: when you need to store an object in Redis, you can use a hash rather than serializing it into a string. For a typical object, Redis will store it using a ziplist. This way, you can achieve get&#x2F;set operations on individual fields with minimal resource overhead.</p><h2 id="Sets"><a href="#Sets" class="headerlink" title="Sets"></a>Sets</h2><p>The set, corresponding to Redis’s <code>S</code> series commands like <code>SADD</code>, <code>SMEMBERS</code>, etc., is a collection of unique elements. Similarly, Redis sets have two implementation methods: <code>intset</code> and hash table.</p><p>Redis sets provide convenient methods for computing intersections, unions, and differences of sets, which can be used in scenarios such as finding common friends.</p><h2 id="Sorted-Sets"><a href="#Sorted-Sets" class="headerlink" title="Sorted Sets"></a>Sorted Sets</h2><p>Sorted sets correspond to Redis’s <code>Z</code> series commands like <code>ZADD</code>, <code>ZRANK</code>, etc., with the underlying implementation being either a ziplist or a skip list. Having read the previous article’s introduction to these two data structures, it should be easy to understand how Redis implements sorted sets.</p><p>Sorted sets are a very commonly used object type, applicable to scenarios such as leaderboards and Top-K problems.</p><h2 id="HyperLogLog"><a href="#HyperLogLog" class="headerlink" title="HyperLogLog"></a>HyperLogLog</h2><p>HyperLogLog in Redis is a data structure used for cardinality estimation — counting the number of distinct elements. Its implementation is based on probabilistic algorithms, achieving counting over large data volumes with minimal memory usage through an acceptable margin of error. The specific algorithm is quite elegant, and I’ll dedicate a separate article to it later. Here’s a quick overview of its capability: HyperLogLog can count up to 2^64 distinct values using only 12KB of memory, with a standard error of 0.81%.</p><p>Using HyperLogLog in Redis is very simple, with just three commands: <code>PFADD</code> to add elements, <code>PFCOUNT</code> to get the current cardinality, and <code>PFMERGE</code> to merge multiple HyperLogLogs.</p><h2 id="GEO"><a href="#GEO" class="headerlink" title="GEO"></a>GEO</h2><p>As the name suggests, the GEO object in Redis is used for handling location-related information. Redis’s GEO object provides functions for adding and viewing location information, calculating distances between two locations, and finding elements within a range.</p><p>The GEO object is built on top of a sorted set (zset) internally. The core mechanism involves encoding two-dimensional latitude and longitude coordinates into a one-dimensional hash value using the GeoHash algorithm, while ensuring that geographically closer coordinates produce closer hash values. We’ll cover GeoHash in detail in a future article. After GeoHash encoding, the underlying zset functionality is leveraged for range queries and similar operations.</p><p>Many internet services today choose Redis for their location-based features, such as “nearby people” or “nearby restaurants.”</p><p>Through the above introduction, I hope it helps you better understand and use Redis. As you can see, Redis performs extreme memory optimizations for specific scenarios, and when using Redis, we should consider how to take better advantage of these optimizations.</p><p>Source: <a href="https://lichuanyang.top/en/posts/25564/">https://lichuanyang.top/en/posts/25564/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/25564/</id>
    <link href="https://lichuanyang.top/en/posts/25564/"/>
    <published>2020-08-08T07:05:48.000Z</published>
    <summary>Source code analysis of Redis object system, covering the underlying implementation of strings, lists, hashes, sets, sorted sets and other objects.</summary>
    <title>Redis Objects</title>
    <updated>2026-06-27T03:50:56.856Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Redis Series" scheme="https://lichuanyang.top/en/categories/Redis-Series/"/>
    <category term="redis" scheme="https://lichuanyang.top/en/tags/redis/"/>
    <category term="data-structures" scheme="https://lichuanyang.top/en/tags/data-structures/"/>
    <category term="skiplist" scheme="https://lichuanyang.top/en/tags/skiplist/"/>
    <content>
      <![CDATA[<p>Redis, as a widely used in-memory database, has made many extreme optimizations at the code level to achieve better performance. An important part of this is the use of underlying data structures. Redis optimizes the usage of different structures based on data volume, data size, and other factors, thereby achieving better operational efficiency and memory usage. Redis’s core data structures include Simple Dynamic Strings, linked lists, dictionaries, skip lists, integer sets, and compressed lists.</p><p>Let’s discuss these data structures one by one.</p><span id="more"></span><h2 id="Simple-Dynamic-Strings-SDS"><a href="#Simple-Dynamic-Strings-SDS" class="headerlink" title="Simple Dynamic Strings (SDS)"></a>Simple Dynamic Strings (SDS)</h2><p>Redis is implemented in C. Let’s first review C — C strings don’t record string length and use a null character to mark the end. This obviously brings three problems: 1. Getting the string length requires O(n) complexity; 2. Careless operations can cause buffer overflow — for example, two strings adjacent in memory, if strcat is called on the former to concatenate other strings, it will cause overflow; 3. Some special content, such as images, audio, etc., when converted to binary, inevitably contain special characters like null characters, making them unstoreable by C strings — meaning C strings lack binary safety.</p><p>For Redis’s application scenarios, the impact of all these points is actually very significant. Therefore, Redis defines a new structure for storing strings, namely SDS.</p><p>The core idea of SDS is to use an extra field to record the string length, which elegantly solves all three problems mentioned above.</p><p>Additionally, Redis has optimized SDS at the code level starting from version 4.0, improving memory usage without affecting its underlying logic.</p><p>This is the SDS source code in Redis 3.0:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">struct sdshdr &#123;</span><br><span class="line">    unsigned <span class="type">int</span> len;</span><br><span class="line">    unsigned <span class="type">int</span> free;</span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>And this is the SDS source code in Redis 4.0 and later:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">  struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr5 &#123;</span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, and 5 msb of string length */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr8 &#123;</span><br><span class="line">    uint8_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint8_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr16 &#123;</span><br><span class="line">    uint16_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint16_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr32 &#123;</span><br><span class="line">    uint32_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint32_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr64 &#123;</span><br><span class="line">    uint64_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint64_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">...</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>As you can see, in the new version of source code, data storage uses different types such as uint8, uint16, etc., depending on the situation. In C, an int occupies 4 bytes, so for the original SDS, even when storing very little information, it would still occupy at least 8 bytes. But uint8 only occupies one byte, and uint16 only occupies 2 bytes. For small data, Redis’s memory usage is significantly optimized.</p><p>Additionally, Redis has mechanisms like space pre-allocation and lazy release to reduce the number of memory allocations. The SDS implementation also ensures that most methods are compatible with C strings, reducing a lot of implementation cost.</p><h2 id="Linked-Lists"><a href="#Linked-Lists" class="headerlink" title="Linked Lists"></a>Linked Lists</h2><p>The linked list in Redis is an ordinary doubly linked list, which I believe everyone is familiar with, so I won’t go into details. The structure is as follows:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">typedef struct listNode &#123;</span><br><span class="line"></span><br><span class="line">    struct listNode *prev;</span><br><span class="line"></span><br><span class="line">    struct listNode *next;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">void</span> *value;</span><br><span class="line"></span><br><span class="line">&#125; listNode;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>The list object in Redis uses a linked list at the underlying level.</p><h2 id="Dictionary"><a href="#Dictionary" class="headerlink" title="Dictionary"></a>Dictionary</h2><p>A dictionary is what we commonly call a map.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">typedef struct dictht &#123;</span><br><span class="line"></span><br><span class="line">    dictEntry **table;</span><br><span class="line"></span><br><span class="line">    unsigned <span class="type">long</span> size; <span class="comment">//hash table length</span></span><br><span class="line"></span><br><span class="line">    unsigned <span class="type">long</span> sizemask;</span><br><span class="line"></span><br><span class="line">    unsigned <span class="type">long</span> used; <span class="comment">//existing length</span></span><br><span class="line"></span><br><span class="line">&#125; dictht;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>The dictionary in Redis is a hash table, using separate chaining to resolve hash address collisions.</p><p>Similar to HashMap in languages like Java, Redis’s dictionary also has a rehash mechanism to keep its load factor within a reasonable range.</p><h2 id="Skip-List-skiplist"><a href="#Skip-List-skiplist" class="headerlink" title="Skip List (skiplist)"></a>Skip List (skiplist)</h2><p>Skip list is a widely used data structure, typically serving as an alternative to AVL trees. Like AVL trees, the search complexity of skiplist is O(logn), but the implementation is much simpler. In just a few lines below, we can explain all the content of SkipList very clearly. Additionally, in concurrent environments, SkipList has significant advantages, because during the balancing process of AVL trees, many nodes may be involved, requiring many nodes to be locked. SkipList doesn’t have this problem at all.</p><p><img src="/en/img/redis/skiplist.jpg" alt="skiplist"></p><p>I found a diagram online that clearly illustrates the structure of a SkipList. A skip list is essentially a multi-level list, where each element randomly appears at certain levels, and the linked list at a given level contains all elements at or above that level.</p><p>Search in a skip list starts from the highest level, gradually descending levels to locate the specific element. For example, to find 7, the sequence would be 9-&gt;6-&gt;7.</p><p>Insertion in a skip list also starts with a search, then directly assigns a random level to the element, and adjusts pointers.</p><p>Deletion involves removing the node and then adjusting pointers.</p><p>The ordered set (sorted set) in Redis is implemented based on skip lists.</p><h2 id="Integer-Set-intset-and-Compressed-List-ziplist"><a href="#Integer-Set-intset-and-Compressed-List-ziplist" class="headerlink" title="Integer Set (intset) and Compressed List (ziplist)"></a>Integer Set (intset) and Compressed List (ziplist)</h2><p>These two structures are very similar, so I’ll discuss them together. They are both specifically optimized for small datasets under specific conditions.</p><p>An integer set is an ordered collection, used when the collection contains only integers and the number of elements is not large.</p><p>A compressed list is similarly used when the list has very few items, and the elements can only be small integer values or short strings. It can provide functionality similar to a doubly linked list.</p><p>Since both integer sets and compressed lists are designed for small datasets, they can use contiguous memory space for storage, making the implementation much simpler. I won’t go into details here.</p><p>In practice, ziplist can serve as an alternative to linked lists or dictionaries, and is used in Redis’s lists, hashes, and sorted sets. Integer sets serve as alternatives to dictionaries and are used in set objects.</p><p>The above are the main data structures in Redis. Based on these structures, Redis implements a large number of fully-featured objects for us to use. Understanding the principles of these underlying structures in Redis can also help us make better use of Redis’s value.</p><p>Source: <a href="https://lichuanyang.top/en/posts/22179/">https://lichuanyang.top/en/posts/22179/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/22179/</id>
    <link href="https://lichuanyang.top/en/posts/22179/"/>
    <published>2020-07-11T08:47:30.000Z</published>
    <summary>Detailed explanation of Redis's underlying data structures, including SDS, ziplist, quicklist, dict, skiplist and other core data structure design and optimization.</summary>
    <title>Redis Data Structures</title>
    <updated>2026-06-27T03:49:54.762Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Architecture Design" scheme="https://lichuanyang.top/en/categories/Architecture-Design/"/>
    <category term="design-patterns" scheme="https://lichuanyang.top/en/tags/design-patterns/"/>
    <content>
      <![CDATA[<p>As everyone knows, design patterns are an extremely important discipline for programmers. However, because design patterns cover such a broad scope and involve many abstract concepts, they can be quite difficult to master. The best way to learn design patterns is to combine them with practical experience. Every time you work on a requirement — especially complex ones, or when you detect signs of smelly code — you can flip through the design patterns to see which ones might apply. Therefore, I’ve summarized this article using the most concise language to describe the main design patterns, possibly the key parts of their definitions, typical application scenarios, or just an English term. The goal is to help recall the purpose and application scenarios of each design pattern.</p><span id="more"></span><h2 id="Basic-Design-Principles"><a href="#Basic-Design-Principles" class="headerlink" title="Basic Design Principles"></a>Basic Design Principles</h2><p>Before discussing design patterns, we must first go over the six fundamental design principles. Simply put, design patterns are a series of best practices distilled from these principles.</p><h2 id="Single-Responsibility-Principle"><a href="#Single-Responsibility-Principle" class="headerlink" title="Single Responsibility Principle"></a>Single Responsibility Principle</h2><h2 id="Open-Closed-Principle"><a href="#Open-Closed-Principle" class="headerlink" title="Open-Closed Principle"></a>Open-Closed Principle</h2><p>Solve problems through extension rather than modifying existing implementations. (When writing code, you can assume changes won’t occur; when changes do arise, abstraction is needed to isolate future similar changes.) As long as modifications don’t break existing unit tests, the Open-Closed Principle is not violated.</p><h2 id="Liskov-Substitution-Principle"><a href="#Liskov-Substitution-Principle" class="headerlink" title="Liskov Substitution Principle"></a>Liskov Substitution Principle</h2><p>Subclasses should be able to substitute for their parent classes.</p><h2 id="Dependency-Inversion-Principle"><a href="#Dependency-Inversion-Principle" class="headerlink" title="Dependency Inversion Principle"></a>Dependency Inversion Principle</h2><p>High-level modules should not depend on low-level modules; both should depend on abstractions rather than implementations.</p><h2 id="Interface-Segregation-Principle"><a href="#Interface-Segregation-Principle" class="headerlink" title="Interface Segregation Principle"></a>Interface Segregation Principle</h2><p>Interfaces should be as small as possible.</p><h2 id="Law-of-Demeter"><a href="#Law-of-Demeter" class="headerlink" title="Law of Demeter"></a>Law of Demeter</h2><p>Decouple classes from each other.</p><h1 id="Design-Patterns"><a href="#Design-Patterns" class="headerlink" title="Design Patterns"></a>Design Patterns</h1><h2 id="Factory-Method-Pattern"><a href="#Factory-Method-Pattern" class="headerlink" title="Factory Method Pattern"></a>Factory Method Pattern</h2><p>Define an interface for creating an object.</p><h2 id="Abstract-Factory-Pattern"><a href="#Abstract-Factory-Pattern" class="headerlink" title="Abstract Factory Pattern"></a>Abstract Factory Pattern</h2><p>Create a family of related or interdependent objects.</p><h2 id="Singleton-Pattern"><a href="#Singleton-Pattern" class="headerlink" title="Singleton Pattern"></a>Singleton Pattern</h2><p>A class has only one instance.</p><h2 id="Builder-Pattern"><a href="#Builder-Pattern" class="headerlink" title="Builder Pattern"></a>Builder Pattern</h2><p>Build each module of an instance separately.</p><h2 id="Prototype-Pattern"><a href="#Prototype-Pattern" class="headerlink" title="Prototype Pattern"></a>Prototype Pattern</h2><p>Copy objects.</p><h2 id="Adapter-Pattern"><a href="#Adapter-Pattern" class="headerlink" title="Adapter Pattern"></a>Adapter Pattern</h2><p>Handle interface incompatibility.</p><h2 id="Decorator-Pattern"><a href="#Decorator-Pattern" class="headerlink" title="Decorator Pattern"></a>Decorator Pattern</h2><p>Dynamically add additional responsibilities to an object, more flexible than creating subclasses.</p><h2 id="Proxy-Pattern"><a href="#Proxy-Pattern" class="headerlink" title="Proxy Pattern"></a>Proxy Pattern</h2><p>Provide a proxy for some objects to restrict access to other objects.</p><h2 id="Facade-Pattern-facade"><a href="#Facade-Pattern-facade" class="headerlink" title="Facade Pattern (facade)"></a>Facade Pattern (facade)</h2><p>The facade contains some or all methods of several subsystems, combining the subsystems and providing them externally.</p><h2 id="Bridge-Pattern"><a href="#Bridge-Pattern" class="headerlink" title="Bridge Pattern"></a>Bridge Pattern</h2><p>Separate the abstraction from the implementation. An abstract class depends on an interface, and both parts can be extended independently.</p><h2 id="Composite-Pattern"><a href="#Composite-Pattern" class="headerlink" title="Composite Pattern"></a>Composite Pattern</h2><p>Treat a group of similar objects as a single object.</p><h2 id="Flyweight-Pattern"><a href="#Flyweight-Pattern" class="headerlink" title="Flyweight Pattern"></a>Flyweight Pattern</h2><p>Shared objects — database connection pools, thread pools, etc. are all applications of the flyweight pattern.</p><h2 id="Strategy-Pattern"><a href="#Strategy-Pattern" class="headerlink" title="Strategy Pattern"></a>Strategy Pattern</h2><p>Dynamically switch between multiple different “strategies” to change an object’s behavior.</p><h2 id="Template-Method-Pattern"><a href="#Template-Method-Pattern" class="headerlink" title="Template Method Pattern"></a>Template Method Pattern</h2><p>The parent class defines the skeleton of the algorithm, with some details implemented by subclasses.</p><h2 id="Observer-Pattern"><a href="#Observer-Pattern" class="headerlink" title="Observer Pattern"></a>Observer Pattern</h2><p>For one-to-many dependencies, when an object’s state changes, all objects that depend on it are notified.</p><h2 id="Iterator-Pattern"><a href="#Iterator-Pattern" class="headerlink" title="Iterator Pattern"></a>Iterator Pattern</h2><p>Provide objects with an iterator.</p><h2 id="Chain-of-Responsibility-Pattern"><a href="#Chain-of-Responsibility-Pattern" class="headerlink" title="Chain of Responsibility Pattern"></a>Chain of Responsibility Pattern</h2><p>Multiple objects have the opportunity to handle a request, connected in a chain. The request is passed along the chain until one of the objects handles it.</p><h2 id="Command-Pattern"><a href="#Command-Pattern" class="headerlink" title="Command Pattern"></a>Command Pattern</h2><p>Pass client requests into an object, achieving decoupling between the “behavior requester” and the “behavior implementer.”</p><h2 id="Memento-Pattern"><a href="#Memento-Pattern" class="headerlink" title="Memento Pattern"></a>Memento Pattern</h2><p>A memento object is an object used to store a snapshot of another object’s internal state.</p><h2 id="State-Pattern"><a href="#State-Pattern" class="headerlink" title="State Pattern"></a>State Pattern</h2><p>State machine.</p><h2 id="Visitor-Pattern"><a href="#Visitor-Pattern" class="headerlink" title="Visitor Pattern"></a>Visitor Pattern</h2><p>Separate data structures from data operations.</p><h2 id="Mediator-Pattern"><a href="#Mediator-Pattern" class="headerlink" title="Mediator Pattern"></a>Mediator Pattern</h2><p>Encapsulate a series of object interactions using a mediator object. The mediator makes objects interact without explicit references to each other.</p><h2 id="Interpreter-Pattern"><a href="#Interpreter-Pattern" class="headerlink" title="Interpreter Pattern"></a>Interpreter Pattern</h2><p>Given a language, define a representation of its grammar and define an interpreter that uses this representation to interpret sentences in the language. Typical scenarios include SQL parsing, etc.</p><p>Source: <a href="https://lichuanyang.top/en/posts/21897/">https://lichuanyang.top/en/posts/21897/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/21897/</id>
    <link href="https://lichuanyang.top/en/posts/21897/"/>
    <published>2020-04-08T10:06:51.000Z</published>
    <summary>A quick reference guide for design patterns, describing the core concepts, typical scenarios, and key English terminology for all 23 patterns in concise language.</summary>
    <title>Design Patterns Cheatsheet</title>
    <updated>2026-06-08T07:50:49.107Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="reflections" scheme="https://lichuanyang.top/en/tags/reflections/"/>
    <content>
      <![CDATA[<p>Over the years of working, I’ve been constantly thinking about what a programmer actually does. As I’ve gained more experience, my understanding has continuously evolved. Writing this article now serves two purposes: sharing my thoughts, and leaving a record to revisit in a couple of years to see how my understanding has further changed.</p><span id="more"></span><h2 id="The-Essence-of-Programming-as-a-Career"><a href="#The-Essence-of-Programming-as-a-Career" class="headerlink" title="The Essence of Programming as a Career"></a>The Essence of Programming as a Career</h2><p>I want to discuss this from two perspectives. First, from the work itself — in my view, a programmer’s work falls into three categories: discovering problems, analyzing problems, and solving problems. In fact, the vast majority of jobs in the world can be encompassed within these three types of work. Second, from a capability standpoint, I divide abilities into two kinds: the ability to understand the real world, and the ability to map the real world into programming languages.</p><p>Regarding the three types of work, in order from discovering to analyzing to solving problems, they progress from macro-level to detail-level. Day-to-day work primarily involves solving problems — all kinds of miscellaneous tasks are fundamentally about solving problems. Discovering and analyzing problems require deeper thinking, but in terms of work itself, I don’t think there’s any hierarchy among these three types. Being able to consistently provide appropriate solutions to identified problems is a formidable core competitiveness in the workplace. As for the two types of capabilities, people generally focus more on the second one. However, I believe the ability to understand the real world is extremely important for a programmer’s career development. Think about it — when it comes to working on business logic or writing CRUD operations, is there a threshold? I think there is — doing business logic well has a very high threshold. Some people always implement business logic in peculiar ways and then blame product managers for unclear requirements, when the real issue may be a lack of understanding of the real world.</p><p>Let me elaborate on each of the three types in order.</p><p><strong>Discovering problems</strong> isn’t strictly necessary for programmers. Many problems, even if you don’t discover them — or discover them and pretend they don’t exist — the system can still run fine. But gradually accumulating such problems will inevitably lead to unmaintainable code. Let me illustrate with a real experience. I once worked on a project involving passenger flight records. We could obtain data for almost all domestic flights. When storing the data, we used ID document numbers as the primary key — because all booking systems use document numbers as their key. But our product displayed data from the user dimension, and a single user could have multiple documents. This meant that even very simple queries — like retrieving a user’s most recent flight record or the last ten records — required complex processing. And as data volume grew, sharding was needed, but documents belonging to the same user couldn’t fall into the same table, adding another level of logical complexity. At this point, not discovering the problem wouldn’t prevent the business from functioning, but working on this piece of business would be extremely painful. Only after recognizing the problem and making adjustments did I realize how simple this business logic actually was.</p><p>So how do we discover problems? This requires the ability to understand the real world, as well as sufficient understanding of fundamental concepts like design patterns. To discover problems, we first need a notion of what the “normal state” should look like — anything deviating from that normal state is a problem. In the earlier example, one “normal state” is that querying a user’s recent flight records should be very simple. Another “normal state” is that our user is a person, not a document. When we noticed the deviation from the normal state, we discovered the problem. Some other examples: writing unit tests should be straightforward — when you find unit tests are hard to write, you need to consider whether the code design has issues. A MySQL batch request should take milliseconds, and data volume shouldn’t significantly affect duration — when you notice the relationship between data volume and latency becoming a steep linear function, consider whether you’re actually performing a batch operation (see <a href="https://lichuanyang.top/posts/63688">https://lichuanyang.top/posts/63688</a>).</p><p><strong>Analyzing problems</strong> is essentially decomposing a manifesting issue into one or more “metrics” that have gone wrong. These “metrics” can be actual measurements like memory or CPU, or they can be basic conceptual understandings and design approaches. In the earlier example, the problem we discovered was that very simple requirements were difficult to implement. The “metric” that eventually turned out to be the root cause was the choice of primary key for storing records.</p><p>For analyzing problems, you should make reasonable conjectures and then use tools to verify or disprove your guesses. Once I discovered data wasn’t being saved to the database. I could form several reasonable hypotheses: the database itself had an issue; the connection to the database was problematic; the transaction wasn’t committed normally; logic errors prevented data from being generated; and so on. Then I checked the logs for errors, looked at MySQL monitoring for uncommitted transactions. Step by step, the problem was pinpointed.</p><h2 id="Depth-vs-Breadth"><a href="#Depth-vs-Breadth" class="headerlink" title="Depth vs Breadth"></a>Depth vs Breadth</h2><p><strong>Solving problems</strong> is what most programmers spend the most time doing. For a programmer, solving problems means representing real-world concepts in a computer. The two most critical aspects, in my understanding, are: first, decomposing tasks — breaking a large task into independent subtasks; and second, the ability to quickly understand and use new tools — essentially, continuous learning. Many people complain that there’s too much to learn as a programmer. But you must pay attention to what’s worth spending significant time on and what you only need to skim through. I’ve noticed many people are enthusiastic about learning tools, enjoying writing various versions of Hello World — in my view, this is ineffective learning. Programming learning requires both depth and breadth. Depth means thoroughly researching certain technologies — after studying some, you’ll find that various technologies, which may seem unrelated on the surface (like databases and message queues), actually share many similar concepts and ideas. Once you’ve studied some in depth, you can build your own knowledge system, and then learning new things becomes much faster.</p><p>Because every new technology emerges from the urgent need to solve practical problems. For example, why was ClickHouse created? Because row-storage databases like MySQL aren’t suitable for data analysis, and there was no columnar storage database with both good performance and stability. Similarly, Jedis connecting directly to Redis Server doesn’t perform well in multi-threaded environments, which is why Lettuce was created. Once you understand what problem needs to be solved, try thinking about what solution you would propose. Then look at the actual solution. If your approach is similar — congratulations, your understanding of technology has reached a new level. If it’s different — congratulations again, you’ve learned a new way of thinking. These insights can be applied whenever you work on business logic in the future.</p><h2 id="My-Career-Philosophy"><a href="#My-Career-Philosophy" class="headerlink" title="My Career Philosophy"></a>My Career Philosophy</h2><p>Let’s keep growing together.</p><p>Source: <a href="https://lichuanyang.top/en/posts/27398/">https://lichuanyang.top/en/posts/27398/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/27398/</id>
    <link href="https://lichuanyang.top/en/posts/27398/"/>
    <published>2020-03-18T09:21:18.000Z</published>
    <summary>Sharing long-term thoughts and cognitive iterations on the programming career from two dimensions: 'what we do' and 'capability model'.</summary>
    <title>Reflections on a Programmer's Career — My Understanding of the Programming Profession</title>
    <updated>2026-06-27T02:17:52.440Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Database" scheme="https://lichuanyang.top/en/categories/Database/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="mysql" scheme="https://lichuanyang.top/en/tags/mysql/"/>
    <category term="batch-not-working" scheme="https://lichuanyang.top/en/tags/batch-not-working/"/>
    <content>
      <![CDATA[<p>As is well known, using batch operations with MySQL can significantly improve performance when dealing with large datasets. However, when using MySQL in Java, certain details must be carefully handled; otherwise, batch operations may not actually take effect, and you won’t benefit from the performance gains of batching.</p><span id="more"></span><h2 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h2><p>Recently, I noticed that the performance profile of a particular SQL query was abnormally slow — inserting several thousand records in batch was taking on the order of seconds. After ruling out server performance and network issues, I suspected that the code was causing the batch operation to not actually be effective. It felt as though each record was being sent to the database individually.</p><h2 id="How-MySQL-JDBC-Batch-Works"><a href="#How-MySQL-JDBC-Batch-Works" class="headerlink" title="How MySQL JDBC Batch Works"></a>How MySQL JDBC Batch Works</h2><p>After digging into the code, I traced the issue to the mysql-connector driver. The key code block looked like this:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!<span class="built_in">this</span>.batchHasPlainStatements &amp;&amp; <span class="built_in">this</span>.connection.getRewriteBatchedStatements()) &#123;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> (canRewriteAsMultiValueInsertAtSqlLevel()) &#123;</span><br><span class="line">                        <span class="keyword">return</span> executeBatchedInserts(batchTimeout);</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> (<span class="built_in">this</span>.connection.versionMeetsMinimum(<span class="number">4</span>, <span class="number">1</span>, <span class="number">0</span>) &amp;&amp; !<span class="built_in">this</span>.batchHasPlainStatements &amp;&amp; <span class="built_in">this</span>.batchedArgs != <span class="literal">null</span></span><br><span class="line">                            &amp;&amp; <span class="built_in">this</span>.batchedArgs.size() &gt; <span class="number">3</span> <span class="comment">/* cost of option setting rt-wise */</span>) &#123;</span><br><span class="line">                        <span class="keyword">return</span> executePreparedBatchAsMultiStatement(batchTimeout);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">return</span> executeBatchSerially(batchTimeout);</span><br></pre></td></tr></table></figure><p>As you can see, there are two main branches: <code>executeBatchedInserts</code> and <code>executeBatchSerially</code>. If execution falls into <code>executeBatchSerially</code>, it’s essentially running a “fake” batch — in this method, each record interacts with MySQL individually.</p><h2 id="Why-Batch-Didn’t-Work"><a href="#Why-Batch-Didn’t-Work" class="headerlink" title="Why Batch Didn’t Work"></a>Why Batch Didn’t Work</h2><p>To successfully enter <code>executeBatchedInserts</code>, several preconditions must be met: <code>batchHasPlainStatements</code>, <code>connection.getRewriteBatchedStatements()</code>, and <code>canRewriteAsMultiValueInsertAtSqlLevel</code>.</p><p>In fact, to successfully execute batch SQL, the MySQL driver performs a “rewrite” on the SQL. What does this rewrite mean? It’s quite simple. Suppose you have a bunch of SQL statements:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span>()</span><br><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span>()</span><br><span class="line">...</span><br><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span>()</span><br></pre></td></tr></table></figure><p>After rewriting, they become:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span> () () ()</span><br></pre></td></tr></table></figure><p>This becomes a single SQL statement, which naturally means only one interaction with MySQL.</p><p>Only after a successful rewrite can batch SQL execution proceed. The conditions listed above are essentially all about enabling SQL rewriting. The two key points here are: first, at the database connection level, you need to configure <code>rewriteBatchedStatements=true</code> to allow SQL rewriting; second, at the SQL level, the SQL must be rewriteable. We know that MySQL has this syntax:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert into</span> () <span class="keyword">set</span> x<span class="operator">=</span>x,y<span class="operator">=</span>y</span><br></pre></td></tr></table></figure><p>This syntax is not part of the SQL standard — it’s a MySQL “dialect” and does not have a batch operation form. Therefore, if you write SQL in this form, you cannot perform true batch operations.</p><p>Source: <a href="https://lichuanyang.top/en/posts/63688/">https://lichuanyang.top/en/posts/63688/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/63688/</id>
    <link href="https://lichuanyang.top/en/posts/63688/"/>
    <published>2020-03-13T09:54:38.000Z</published>
    <summary>Analysis of common reasons why MySQL batch operations fail in Java, along with a complete debugging approach for batch performance issues.</summary>
    <title>Troubleshooting MySQL Batch Operations in Java: Why Batches Sometimes Don't Work</title>
    <updated>2026-06-27T03:53:05.204Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Talk" scheme="https://lichuanyang.top/en/categories/Tech-Talk/"/>
    <category term="unit-testing" scheme="https://lichuanyang.top/en/tags/unit-testing/"/>
    <category term="development-standards" scheme="https://lichuanyang.top/en/tags/development-standards/"/>
    <category term="unit-testing best practices" scheme="https://lichuanyang.top/en/tags/unit-testing-best-practices/"/>
    <content>
      <![CDATA[<p>A while ago, when I was trying to improve the unit tests for a project, I realized that I never had a particularly clear understanding of how to write unit tests. So I looked up some materials, and in the process discovered that everyone’s perspectives are quite different. Every article I read updated my own understanding. This article combines others’ viewpoints with my own practical experience to summarize what I currently believe to be correct. It probably won’t be the most definitive answer, and I welcome everyone to leave comments and discuss.</p><span id="more"></span><p>Let me first state the purpose of writing unit tests, which is the foundation for all the following discussion. The significance of unit tests should be to ensure the correctness of existing code when modifications are made. In other words, after modifying code, running through the existing test cases — if they pass, it means the previous logic is still working.</p><p>Below, I’ll list specific principles that I believe should be followed when writing unit tests.</p><h3 id="Avoid-Dependencies-on-the-Environment"><a href="#Avoid-Dependencies-on-the-Environment" class="headerlink" title="Avoid Dependencies on the Environment"></a>Avoid Dependencies on the Environment</h3><p>Unit tests should not depend on the environment, including dependencies on the network, database, I&#x2F;O, data, external interfaces, etc. This point should be relatively uncontroversial, because unit tests have many execution scenarios — such as manual triggering by developers, automatic triggering when code is committed, automatic triggering during packaging, etc. If unit tests can’t eliminate environmental dependencies, their execution scenarios will be greatly limited.</p><p>Achieving this requires the help of many tools, such as in-memory databases, embedded Redis servers, mocking, etc. This will inevitably add a lot of work to the unit test writing process, but it’s unavoidable.</p><h3 id="Test-Business-Logic-Not-Implementation"><a href="#Test-Business-Logic-Not-Implementation" class="headerlink" title="Test Business Logic, Not Implementation"></a>Test Business Logic, Not Implementation</h3><p>Tests should target business logic, not implementation logic. This is a point that will generate more debate, mainly because people can’t agree on the definition of “unit.” Some believe that a single function is a “unit,” but if you follow this standard for unit testing, the tests won’t achieve the expected results.</p><p>A simple example: when refactoring code, should you change the unit tests? The answer is no. If every code refactoring requires corresponding changes to unit tests, the value of unit tests is largely lost. Therefore, the “unit” being tested should be a business unit. Of course, this shouldn’t be too rigid — for example, writing a unit test for a complex method is perfectly fine. A more accurate approach is: write unit tests based on intent, not based on implementation. In other words, treat the code’s implementation logic as a black box and only test inputs and outputs.</p><h3 id="Minimize-Mocking"><a href="#Minimize-Mocking" class="headerlink" title="Minimize Mocking"></a>Minimize Mocking</h3><p>In the first point above, we mentioned using mocks to avoid external environment dependencies. But mocking must not be overused. You can read about <a href="https://gitbook.cn/books/58fa1af500a2684bf77511bc/index.html">The Seven Deadly Sins of Mocking</a>. The principle is: don’t mock if you don’t have to. When is mocking necessary? When you need to depend on external environments. In all other cases, don’t mock.</p><h3 id="Unit-Tests-Drive-Refactoring"><a href="#Unit-Tests-Drive-Refactoring" class="headerlink" title="Unit Tests Drive Refactoring"></a>Unit Tests Drive Refactoring</h3><p>When writing unit tests, you’ll find that some tests are very difficult to write. Analyzing the specific reasons for this difficulty, you’ll find that sometimes it’s because the code itself is poorly written. For example, if a class has too many dependencies, it might be because the class’s responsibilities are unclear and unrelated code has been lumped together. At this point, you should directly refactor the code, splitting it into several classes, and the difficulty of writing unit tests will naturally decrease.</p><p>Source: <a href="https://lichuanyang.top/en/posts/695/">https://lichuanyang.top/en/posts/695/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/695/</id>
    <link href="https://lichuanyang.top/en/posts/695/"/>
    <published>2019-09-26T12:54:36.000Z</published>
    <summary>Combining prior perspectives and practical experience, this article summarizes how to write unit tests — a systematic guide from purpose and principles to concrete implementation.</summary>
    <title>Unit Testing Best Practices: Practical Experience and Guidelines</title>
    <updated>2026-06-08T07:50:49.088Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="rocketmq" scheme="https://lichuanyang.top/en/tags/rocketmq/"/>
    <category term="message-queue" scheme="https://lichuanyang.top/en/tags/message-queue/"/>
    <content>
      <![CDATA[<p>RocketMQ’s MessageQueue has a unique design: it splits a queue into <code>readQueueNums</code> (read queue count) and <code>writeQueueNums</code> (write queue count). In the vast majority of cases, these two values must be equal — if they diverge, serious problems arise. So why separate them? The answer lies in <strong>smooth scaling</strong>.</p><span id="more"></span><h2 id="What-is-a-MessageQueue"><a href="#What-is-a-MessageQueue" class="headerlink" title="What is a MessageQueue?"></a>What is a MessageQueue?</h2><p>In RocketMQ, a Topic is a logical category for messages, while a MessageQueue is the physical shard of a Topic, similar to Kafka’s Partition. A key constraint: <strong>the number of Consumers must not exceed the number of Queues</strong> — because each Queue can only be consumed by one Consumer at a time.</p><h2 id="Normal-State-readQueue-writeQueue"><a href="#Normal-State-readQueue-writeQueue" class="headerlink" title="Normal State: readQueue &#x3D; writeQueue"></a>Normal State: readQueue &#x3D; writeQueue</h2><p>Under normal conditions, <code>readQueueNums == writeQueueNums</code>. Producers write messages to all Queues, and Consumers read from all Queues — everything works as expected.</p><p>But if the two values are inconsistent:</p><table><thead><tr><th>State</th><th>Consequence</th></tr></thead><tbody><tr><td>writeQueue &gt; readQueue</td><td>Some Queues are write-only with no reads → <strong>messages will never be consumed, causing a backlog</strong></td></tr><tr><td>readQueue &gt; writeQueue</td><td>Some Consumers spin idly → <strong>wasted resources, will never receive messages</strong></td></tr></tbody></table><p>If consistency works and inconsistency breaks things, what’s the point of separating them?</p><h2 id="Core-Value-Smooth-Scaling"><a href="#Core-Value-Smooth-Scaling" class="headerlink" title="Core Value: Smooth Scaling"></a>Core Value: Smooth Scaling</h2><h3 id="Scaling-Up-4-→-8-as-an-example"><a href="#Scaling-Up-4-→-8-as-an-example" class="headerlink" title="Scaling Up (4 → 8 as an example)"></a>Scaling Up (4 → 8 as an example)</h3><p>Without read&#x2F;write separation, the dilemma: modifying the Queue count takes effect instantly, leading to the following problems:</p><ul><li>If Producers are notified of new Queues first, they start writing to new Queues, but Consumers haven’t picked them up yet → <strong>writes with no reads</strong></li><li>If Consumers are notified first, they start listening on new Queues, but Producers haven’t started writing yet → <strong>Consumers spin idly</strong></li></ul><p>With read&#x2F;write separation, a <strong>two-step scale-up</strong>:</p><table><thead><tr><th>Step</th><th>Operation</th><th>State</th><th>Effect</th></tr></thead><tbody><tr><td><strong>Step 1</strong></td><td><code>readQueueNums: 4→8</code></td><td>write&#x3D;4, read&#x3D;8</td><td>Add 4 new Consumers, start listening on new Queues (no messages on new Queues yet, idling causes no harm)</td></tr><tr><td><strong>Step 2</strong></td><td><code>writeQueueNums: 4→8</code></td><td>write&#x3D;8, read&#x3D;8</td><td>Producers start writing to new Queues, Consumers are already waiting — messages are consumed immediately</td></tr></tbody></table><p><strong>The entire process has no message loss and no Consumer resource waste.</strong></p><p>If you reverse the two steps (change write first, then read), the result is the same — since Consumer scaling is the slowest part (requires starting new instances), it makes the most sense to complete the scale-up first, then start writing messages.</p><h3 id="Scaling-Down-8-→-4-as-an-example"><a href="#Scaling-Down-8-→-4-as-an-example" class="headerlink" title="Scaling Down (8 → 4 as an example)"></a>Scaling Down (8 → 4 as an example)</h3><table><thead><tr><th>Step</th><th>Operation</th><th>State</th><th>Effect</th></tr></thead><tbody><tr><td><strong>Step 1</strong></td><td><code>writeQueueNums: 8→4</code></td><td>write&#x3D;4, read&#x3D;8</td><td>Producers stop writing to the Queues being decommissioned (but Consumers are still reading, existing messages won’t be lost)</td></tr><tr><td><strong>Step 2</strong></td><td><code>readQueueNums: 8→4</code></td><td>write&#x3D;4, read&#x3D;4</td><td>After all messages in old Queues are consumed, Consumers scale down</td></tr></tbody></table><h2 id="Comparison-Kafka’s-Partition-Scaling"><a href="#Comparison-Kafka’s-Partition-Scaling" class="headerlink" title="Comparison: Kafka’s Partition Scaling"></a>Comparison: Kafka’s Partition Scaling</h2><p>In Kafka, the number of Partitions <strong>can only be increased, never decreased</strong>. Scaling up is relatively straightforward — after adding Partitions, Consumers become aware of them automatically through Rebalance. But scaling down is simply not supported in Kafka. Once the Partition count is set too high, you’re stuck with it.</p><p>By separating readQueue and writeQueue, RocketMQ supports <strong>bidirectional smooth scaling</strong> — a design refined through production experience at Alibaba’s massive scale.</p><h2 id="Diagram"><a href="#Diagram" class="headerlink" title="Diagram"></a>Diagram</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Normal state: write=4, read=4</span><br><span class="line">Producer ——→ [Q0][Q1][Q2][Q3] ——→ Consumer Group (4 instances)</span><br><span class="line">              ✓   ✓   ✓   ✓</span><br><span class="line"></span><br><span class="line">Scale-up Step 1: write=4, read=8</span><br><span class="line">Producer ——→ [Q0][Q1][Q2][Q3] ——→ Consumer Group (8 instances)</span><br><span class="line">              ✓   ✓   ✓   ✓       [Q4-Q7 waiting idle]</span><br><span class="line"></span><br><span class="line">Scale-up Step 2: write=8, read=8</span><br><span class="line">Producer ——→ [Q0][Q1][Q2][Q3][Q4][Q5][Q6][Q7] ——→ Consumer Group (8 instances)</span><br><span class="line">              ✓   ✓   ✓   ✓   ✓   ✓   ✓   ✓</span><br></pre></td></tr></table></figure><h2 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h2><p>The separation of <code>readQueueNums</code> and <code>writeQueueNums</code> is the key design that enables <strong>lossless scaling</strong> in RocketMQ. In day-to-day operations, these two values should remain equal, only allowed to diverge during the brief window of a scaling operation. This design may seem to violate the intuitive principle that “reads and writes should be consistent,” but it is precisely this “temporary, controlled inconsistency” that makes the scaling process completely smooth.</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/32580/</id>
    <link href="https://lichuanyang.top/en/posts/32580/"/>
    <published>2019-05-18T07:23:02.000Z</published>
    <summary>Explains why RocketMQ splits MessageQueue into readQueue and writeQueue in its design.</summary>
    <title>About RocketMQ's readQueue and writeQueue</title>
    <updated>2026-06-27T02:42:04.193Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Database" scheme="https://lichuanyang.top/en/categories/Database/"/>
    <category term="big-data" scheme="https://lichuanyang.top/en/tags/big-data/"/>
    <category term="clickhouse" scheme="https://lichuanyang.top/en/tags/clickhouse/"/>
    <category term="clickhouse-introduction" scheme="https://lichuanyang.top/en/tags/clickhouse-introduction/"/>
    <content>
      <![CDATA[<p>ClickHouse is an open-source database for real-time data analytics developed by Yandex, initially used in multiple data analytics projects internally at Yandex. To introduce ClickHouse, we first need to introduce Yandex. ClickHouse’s emergence is closely related to Yandex’s business needs. Yandex is Russia’s largest search engine, with many data analytics projects. The largest of these by data volume is Yandex.Metrica, a website analytics service similar to Baidu Analytics, with data volume second only to Google Analytics. Since ClickHouse was open-sourced, many companies both domestically and internationally have begun using it in their production systems. Therefore, I’m writing this ClickHouse tutorial to provide a basic introduction.</p><span id="more"></span><h2 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h2><p>ClickHouse is extremely well-suited for OLAP (Online Analytical Processing) problems. These types of problems have the following characteristics:</p><ul><li>Requests are predominantly reads; data additions and updates are generally performed in batches;</li><li>Tables can be very wide, but actual queries typically only involve a limited number of columns;</li><li>Column values are generally small, typically numbers or short strings;</li><li>The query result set is significantly smaller than the source data;</li><li>Transaction processing requirements are relatively weak</li></ul><p>According to the performance benchmarks provided by ClickHouse, its performance far exceeds all competing products.</p><p>The core approach used for this type of problem is columnar storage. Commonly used databases like MySQL use row-based storage. Columnar storage, simply put, stores data from the same column together, as shown in the following diagram.</p><p><img src="/en/img/clickhouse/column.png" alt="Columnar Storage Illustration"></p><p>Compared to row-based storage, columnar storage only reads the relevant columns during queries, reducing I&#x2F;O overhead; any column can serve as an index; however, data writes are relatively more complex. Comparing with the OLAP problem characteristics we described earlier, we can see that columnar storage is extremely well-suited for online data analytics.</p><h2 id="ClickHouse-Storage-Engines"><a href="#ClickHouse-Storage-Engines" class="headerlink" title="ClickHouse Storage Engines"></a>ClickHouse Storage Engines</h2><p>ClickHouse provides many storage engines, but the MergeTree engine is sufficient for over 90% of use cases. Let’s look at this engine in detail.</p><p>Simply put, MergeTree has the following characteristics:</p><ul><li>Data is stored in parts (typically partitioned by date);</li><li>Within each part, data is sorted by primary key and divided into multiple blocks;</li><li>New parts are generated on each insert;</li><li>Asynchronous merging</li></ul><p>First, let’s look at the file structure of MergeTree. (The images in this section are from the ClickHouse Chinese community (<a href="http://clickhouse.com.cn/">http://clickhouse.com.cn/</a>), shared by Teacher Gao Peng from Sina)<br><img src="/en/img/clickhouse/folder.png"><br>The directory like 20171001…. represents a part. The files within include: columns.txt records column information; each column has a bin file and an mrk file, where the bin file contains the actual data. primary.idx stores primary key information, with a structure similar to mrk, like a sparse index.</p><p><img src="/en/img/clickhouse/mergetree.jpg" alt="Detailed Storage Structure"></p><p>This shows the specific structure of mrk and primary files. As we can see, data is sorted by primary key and divided into blocks at regular intervals. Each block also extracts one data point as an index, stored in primary.idx and each column’s mrk file.</p><p>When querying with MergeTree, the most critical step is locating the block. This differs depending on whether the queried column is part of the primary key. Queries on the primary key have better performance, but non-primary-key queries also perform reasonably well due to columnar storage—even though a full scan is performed. So indexes are not as critical in ClickHouse as they are in MySQL. In practice, you generally need to add date-based query conditions to ensure performance for non-primary-key queries.</p><p>After finding the corresponding block, the next step is to search for data within the block, retrieve the needed rows, and assemble the other column data.</p><h2 id="Other-Features"><a href="#Other-Features" class="headerlink" title="Other Features"></a>Other Features</h2><h3 id="Data-Compression"><a href="#Data-Compression" class="headerlink" title="Data Compression"></a>Data Compression</h3><p>Some column-oriented DBMSs (such as InfiniDB CE and MonetDB) do not use data compression. However, data compression plays a significant role in ClickHouse’s performance. For columnar storage, fields of the same type stored together with similar data are much easier to compress. ClickHouse supports compression algorithms such as LZ4 and ZSTD.</p><h3 id="Disk-Based-Storage"><a href="#Disk-Based-Storage" class="headerlink" title="Disk-Based Storage"></a>Disk-Based Storage</h3><p>ClickHouse supports standard disks, giving it a significant cost advantage.</p><p>It provides configurations for RAID, SSD, and large memory, which can be leveraged if available.</p><p>Data is stored in primary key order, enabling queries with extremely low latency.</p><h3 id="Vectorized-Engine"><a href="#Vectorized-Engine" class="headerlink" title="Vectorized Engine"></a>Vectorized Engine</h3><p>Modern CPUs feature a mechanism called SIMD (Single Instruction, Multiple Data), where a single instruction operates on multiple data points. For columnar storage data, this mechanism can be easily leveraged to better utilize CPU performance.</p><h3 id="Multi-Core-and-Multi-Server-Parallel-Processing"><a href="#Multi-Core-and-Multi-Server-Parallel-Processing" class="headerlink" title="Multi-Core and Multi-Server Parallel Processing"></a>Multi-Core and Multi-Server Parallel Processing</h3><p>In ClickHouse, data is stored across different shards. Queries are executed in parallel across multiple shards.</p><p>On each server, multi-core parallel processing is also used to fully utilize single-machine performance.</p><h3 id="Real-Time-Data-Ingestion"><a href="#Real-Time-Data-Ingestion" class="headerlink" title="Real-Time Data Ingestion"></a>Real-Time Data Ingestion</h3><p>With the MergeTree engine, newly inserted data first forms a new part, at which point the data is already queryable. Therefore, the latency from data write to queryability is very small. Subsequently, different parts continue to be merged asynchronously to improve storage efficiency.</p><h3 id="Data-Replication"><a href="#Data-Replication" class="headerlink" title="Data Replication"></a>Data Replication</h3><p>ClickHouse uses ZooKeeper-based master-master replication. After writing to any available replica, data is distributed to all remaining replicas. Replication is performed block by block, and failed replications can be directly retried. The system maintains identical data across different replicas.</p><p>Additionally, compared to other columnar databases, ClickHouse has excellent support for SQL syntax, including common SQL statements like GROUP BY, ORDER BY, IN, and JOIN.</p><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><ol><li><strong>Install ClickHouse</strong>: On Ubuntu: <code>sudo apt-get install clickhouse-server clickhouse-client</code>. Start the service and connect with <code>clickhouse-client</code>.</li><li><strong>Create table</strong>: Use the MergeTree engine, specify <code>ORDER BY</code> (sort key) and <code>PARTITION BY</code> (partition key, typically by date). Define columns as needed — no per-column indexes required like in MySQL.</li><li><strong>Import data</strong>: Use <code>clickhouse-client --query &quot;INSERT INTO table FORMAT CSV&quot; &lt; data.csv</code> or the <code>clickhouse-local</code> tool for bulk import. Data is queryable immediately; merges happen in the background.</li><li><strong>Write queries</strong>: Use standard SQL for analysis. ClickHouse has solid support for <code>GROUP BY</code>, <code>ORDER BY</code>, <code>JOIN</code>, etc. Add date filters to boost non-primary-key query performance.</li><li><strong>Compare MySQL performance</strong>: Load the same data into MySQL and ClickHouse, run identical aggregation queries, and experience the order-of-magnitude difference that columnar storage makes for OLAP workloads.</li></ol><p>Source: <a href="https://lichuanyang.top/en/posts/48312/">https://lichuanyang.top/en/posts/48312/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/48312/</id>
    <link href="https://lichuanyang.top/en/posts/48312/"/>
    <published>2018-11-14T13:59:31.000Z</published>
    <summary>An introductory guide to ClickHouse, a real-time analytics database. Explains ClickHouse's design background and core features starting from Yandex's business needs.</summary>
    <title>A Powerful Tool for Data Analytics: Introduction to ClickHouse</title>
    <updated>2026-06-27T02:41:46.438Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="bitwise-operations" scheme="https://lichuanyang.top/en/tags/bitwise-operations/"/>
    <content>
      <![CDATA[<p>Bitwise operations are among the most fundamental and efficient operations in computer science. Today, let’s explore a classic application: converting between uppercase and lowercase letters with a single line of code.</p><span id="more"></span><h2 id="Why-Use-Bitwise-Operations-for-Case-Conversion"><a href="#Why-Use-Bitwise-Operations-for-Case-Conversion" class="headerlink" title="Why Use Bitwise Operations for Case Conversion?"></a>Why Use Bitwise Operations for Case Conversion?</h2><p>Your first reaction might be: doesn’t Java already have <code>Character.toUpperCase()</code> and <code>Character.toLowerCase()</code>? Why reinvent the wheel?</p><p>Indeed, 99% of the time you should use the standard library. But understanding this trick has several benefits:</p><ol><li><strong>Common interview topic</strong>: Many algorithm interviews test this kind of bitwise thinking</li><li><strong>Understanding low-level computing</strong>: Appreciate the elegance of ASCII code design</li><li><strong>Embedded&#x2F;low-level development</strong>: In environments without standard libraries, this is how it’s done</li><li><strong>Code brevity</strong>: <code>ch ^= 32</code> is far more elegant than <code>Character.isUpperCase(ch) ? Character.toLowerCase(ch) : ch</code></li></ol><h2 id="The-Elegant-Design-of-ASCII"><a href="#The-Elegant-Design-of-ASCII" class="headerlink" title="The Elegant Design of ASCII"></a>The Elegant Design of ASCII</h2><p>To understand this trick, first examine how English letters are encoded in the ASCII table:</p><table><thead><tr><th>Letter</th><th>Decimal</th><th>Hexadecimal</th><th>Binary</th></tr></thead><tbody><tr><td>A</td><td>65</td><td>0x41</td><td><strong>01</strong>00 0001</td></tr><tr><td>a</td><td>97</td><td>0x61</td><td><strong>01</strong>10 0001</td></tr><tr><td>B</td><td>66</td><td>0x42</td><td><strong>01</strong>00 0010</td></tr><tr><td>b</td><td>98</td><td>0x62</td><td><strong>01</strong>10 0010</td></tr><tr><td>Z</td><td>90</td><td>0x5A</td><td><strong>01</strong>01 1010</td></tr><tr><td>z</td><td>122</td><td>0x7A</td><td><strong>01</strong>11 1010</td></tr></tbody></table><p>Notice the pattern: <strong>the ASCII values of uppercase and lowercase letters differ by exactly 32 (0x20)</strong>. At the binary level, this is the difference in the 6th bit (counting from the right, starting at 0):</p><ul><li>Uppercase letters have the 6th bit as <strong>0</strong> (e.g., A: 010<strong>0</strong> 0001)</li><li>Lowercase letters have the 6th bit as <strong>1</strong> (e.g., a: 011<strong>0</strong> 0001)</li></ul><p>This is the beauty of ASCII’s design — case conversion requires flipping just a single bit!</p><h2 id="Three-Bitwise-Approaches"><a href="#Three-Bitwise-Approaches" class="headerlink" title="Three Bitwise Approaches"></a>Three Bitwise Approaches</h2><h3 id="1-XOR-Toggle-Switch-Between-Cases"><a href="#1-XOR-Toggle-Switch-Between-Cases" class="headerlink" title="1. XOR (Toggle): Switch Between Cases"></a>1. XOR (Toggle): Switch Between Cases</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ch ^= <span class="number">32</span>;  <span class="comment">// equivalent to ch ^= 0x20</span></span><br></pre></td></tr></table></figure><p>The <code>^</code> (XOR) rule is “same gives 0, different gives 1”. XOR-ing with 32 (where the 6th bit is 1) flips only that bit: uppercase becomes lowercase, lowercase becomes uppercase.</p><h3 id="2-Force-to-Uppercase"><a href="#2-Force-to-Uppercase" class="headerlink" title="2. Force to Uppercase"></a>2. Force to Uppercase</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ch &amp;= ~<span class="number">32</span>;  <span class="comment">// equivalent to ch &amp;= 0xDF</span></span><br></pre></td></tr></table></figure><p><code>~32</code> (bitwise NOT) makes the 6th bit 0 while leaving all other bits as 1. The <code>&amp;</code> operation clears the 6th bit, so regardless of input case, the output is always uppercase.</p><h3 id="3-Force-to-Lowercase"><a href="#3-Force-to-Lowercase" class="headerlink" title="3. Force to Lowercase"></a>3. Force to Lowercase</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ch |= <span class="number">32</span>;  <span class="comment">// equivalent to ch |= 0x20</span></span><br></pre></td></tr></table></figure><p>The <code>|</code> operation sets the 6th bit to 1. Regardless of input case, the output is always lowercase.</p><h2 id="Edge-Cases"><a href="#Edge-Cases" class="headerlink" title="Edge Cases"></a>Edge Cases</h2><p>The above operations <strong>only work for English letters</strong>. They are meaningless for non-letter characters, so validate first:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (ch &gt;= <span class="string">&#x27;A&#x27;</span> &amp;&amp; ch &lt;= <span class="string">&#x27;Z&#x27;</span> || ch &gt;= <span class="string">&#x27;a&#x27;</span> &amp;&amp; ch &lt;= <span class="string">&#x27;z&#x27;</span>) &#123;</span><br><span class="line">    ch ^= <span class="number">32</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="What-About-Real-World-Performance"><a href="#What-About-Real-World-Performance" class="headerlink" title="What About Real-World Performance?"></a>What About Real-World Performance?</h2><h2 id="On-modern-JVMs-Character-toUpperCase-has-been-optimized-as-an-intrinsic-JVM-level-native-optimization-so-there-is-virtually-no-performance-difference-The-takeaway-understand-this-trick-but-prefer-the-standard-library-in-daily-development-Bitwise-case-conversion-is-most-useful-for-algorithm-problems-embedded-scenarios-and-understanding-open-source-code-that-employs-this-technique"><a href="#On-modern-JVMs-Character-toUpperCase-has-been-optimized-as-an-intrinsic-JVM-level-native-optimization-so-there-is-virtually-no-performance-difference-The-takeaway-understand-this-trick-but-prefer-the-standard-library-in-daily-development-Bitwise-case-conversion-is-most-useful-for-algorithm-problems-embedded-scenarios-and-understanding-open-source-code-that-employs-this-technique" class="headerlink" title="On modern JVMs, Character.toUpperCase() has been optimized as an intrinsic (JVM-level native optimization), so there is virtually no performance difference. The takeaway: understand this trick, but prefer the standard library in daily development. Bitwise case conversion is most useful for algorithm problems, embedded scenarios, and understanding open-source code that employs this technique."></a>On modern JVMs, <code>Character.toUpperCase()</code> has been optimized as an intrinsic (JVM-level native optimization), so there is virtually no performance difference. The takeaway: <strong>understand this trick, but prefer the standard library in daily development</strong>. Bitwise case conversion is most useful for algorithm problems, embedded scenarios, and understanding open-source code that employs this technique.</h2>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/9193/</id>
    <link href="https://lichuanyang.top/en/posts/9193/"/>
    <published>2018-11-14T13:58:47.000Z</published>
    <summary>Leveraging bitwise operations to elegantly convert between uppercase and lowercase letters, with a deep dive into the binary characteristics of ASCII codes.</summary>
    <title>Converting Between Uppercase and Lowercase Using Bitwise Operations</title>
    <updated>2026-06-27T03:51:55.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="ActiveMQ Series" scheme="https://lichuanyang.top/en/categories/ActiveMQ-Series/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="Problem-Scenario"><a href="#Problem-Scenario" class="headerlink" title="Problem Scenario"></a>Problem Scenario</h2><p>In a <a href="https://lichuanyang.top/posts/48216/">previous article</a>, I introduced the basic syntax for using ActiveMQ on the client side.</p><p>It is worth noting the consumer side:</p><span id="more"></span><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">consume</span><span class="params">()</span> <span class="keyword">throws</span> JMSException &#123;</span><br><span class="line">        <span class="type">ActiveMQConnectionFactory</span> <span class="variable">cf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ActiveMQConnectionFactory</span>();</span><br><span class="line">        cf.setBrokerURL(<span class="string">&quot;tcp://localhost:61616&quot;</span>);</span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> cf.createConnection();</span><br><span class="line">        connection.start();</span><br><span class="line">        <span class="type">Session</span> <span class="variable">session</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue</span> <span class="operator">=</span> session.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer</span> <span class="operator">=</span> session.createConsumer(queue);</span><br><span class="line"></span><br><span class="line"><span class="comment">//        Message message = consumer.receive();</span></span><br><span class="line"><span class="comment">//        TextMessage textMessage = (TextMessage) message;</span></span><br><span class="line"><span class="comment">//        System.out.println(textMessage.getText());</span></span><br><span class="line"></span><br><span class="line">        consumer.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer1: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="type">Session</span> <span class="variable">session2</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue2</span> <span class="operator">=</span> session2.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer2</span> <span class="operator">=</span> session2.createConsumer(queue2);</span><br><span class="line">        consumer2.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer2: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue3</span> <span class="operator">=</span> session2.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer3</span> <span class="operator">=</span> session2.createConsumer(queue3);</span><br><span class="line">        consumer3.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer3: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            TimeUnit.MINUTES.sleep(<span class="number">100</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="Multi-threaded-Consumption-Analysis"><a href="#Multi-threaded-Consumption-Analysis" class="headerlink" title="Multi-threaded Consumption Analysis"></a>Multi-threaded Consumption Analysis</h2><p>As you can see, there are actually three ways to achieve multi-threaded consumption of the same queue: defining multiple connections, sessions, and consumers. At the application level, these three approaches produce the same effect — they all generate multiple consumers to process messages in the queue in parallel. However, in terms of performance, there are noticeable differences among these three approaches. Let’s elaborate below.</p><p>First, defining multiple consumers is only a form of <strong>pseudo-parallelism</strong> and does not achieve true concurrent consumption. The reason is that in the JMS protocol, a session can only be used by one thread at a time. So when multiple consumers share the same session, they simply take turns using that one session.</p><p>Multiple sessions and multiple connections, on the other hand, are truly concurrent operations. A connection corresponds to a physical TCP connection. One connection can create multiple sessions, and these sessions share the same TCP connection. The difference between multiple sessions and multiple connections is simply whether you open one or multiple TCP connections. The impact is easy to understand: multiple TCP connections can handle greater network traffic, but of course they also bring some overhead in establishing and maintaining connections.</p><h2 id="Summary-and-Recommendations"><a href="#Summary-and-Recommendations" class="headerlink" title="Summary and Recommendations"></a>Summary and Recommendations</h2><p><strong>In summary</strong>, in most cases, you can define multiple sessions to achieve parallel consumption in ActiveMQ. When traffic is high, you can consider opening multiple connections. As for multiple consumers, I have not yet found a scenario where that would be useful.</p><h2 id="Source-https-lichuanyang-top-en-posts-20459"><a href="#Source-https-lichuanyang-top-en-posts-20459" class="headerlink" title="Source: https://lichuanyang.top/en/posts/20459/"></a>Source: <a href="https://lichuanyang.top/en/posts/20459/">https://lichuanyang.top/en/posts/20459/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/20459/</id>
    <link href="https://lichuanyang.top/en/posts/20459/"/>
    <published>2018-09-16T01:58:11.000Z</published>
    <summary>Comparing three approaches to implementing multi-threading on the ActiveMQ consumer side (multiple connections, multiple sessions, multiple consumers), and analyzing their performance differences.</summary>
    <title>ActiveMQ Multi-threaded Consumption - Different Approaches</title>
    <updated>2026-06-27T03:50:56.860Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Architecture Design" scheme="https://lichuanyang.top/en/categories/Architecture-Design/"/>
    <category term="design" scheme="https://lichuanyang.top/en/tags/design/"/>
    <content>
      <![CDATA[<p>Recently, I participated in optimizing an older billing system and learned some common techniques for balance deduction under high concurrency. I also tried some approaches, so I’m summarizing my findings here.</p><span id="more"></span><h2 id="Problem-Description"><a href="#Problem-Description" class="headerlink" title="Problem Description"></a>Problem Description</h2><p>For a billing system, concurrency issues are actually divided into two categories. The first is high application concurrency—essentially a large number of users and high access volume. This type of problem is no different from general high concurrency problems and can be solved using distributed techniques. The second category is user concurrency issues that generally cannot be solved by distributed methods alone, which is what this article will focus on.</p><p>This type of problem stems from high-frequency access to certain accounts. A large number of concurrent accesses cause bottlenecks to first appear at certain database records. Many operations end up waiting because they cannot acquire row locks on the database, and these waiting operations consume other resources, eventually leading to system unavailability.</p><p>The following sections introduce some common approaches to handle this type of problem.</p><h2 id="Not-Setting-a-Balance-Field"><a href="#Not-Setting-a-Balance-Field" class="headerlink" title="Not Setting a Balance Field"></a>Not Setting a Balance Field</h2><p>Since a stable billing system must record billing transaction details, it’s entirely possible to not have a balance field and instead calculate the balance based on transaction details.</p><p>However, this method is not universal. For example, in an advertising billing system where the frequency is very high and each transaction amount is very small, trying to calculate the balance by summing up transactions is clearly impractical.</p><h2 id="Merging-and-Splitting"><a href="#Merging-and-Splitting" class="headerlink" title="Merging and Splitting"></a>Merging and Splitting</h2><p>These are two different approaches, but since they share some similarities—both aim to reduce access pressure on individual database records—I’ll discuss them together.</p><p>Merging involves consolidating multiple requests for a single account before writing to the database, effectively reducing the pressure by several times.</p><p>Splitting involves breaking a main account into several sub-accounts and distributing requests across them, reducing the pressure on any single account. Then, other techniques are used to merge the sub-account data into the main account data for returning to the user.</p><h2 id="Reducing-Row-Lock-Hold-Time"><a href="#Reducing-Row-Lock-Hold-Time" class="headerlink" title="Reducing Row Lock Hold Time"></a>Reducing Row Lock Hold Time</h2><p>This is a code-level optimization. As mentioned earlier, the reason high-frequency accounts cause system performance issues is the contention for row locks. So if we can reduce the time each request holds a row lock, system performance will improve significantly.</p><p>First, try to speed up the execution between acquiring the row lock and committing the transaction. Move unnecessary operations, especially time-consuming ones, to be executed elsewhere—either before acquiring the row lock or outside the transaction.</p><p>Then, try to avoid acquiring row locks using:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select ... for update</span><br></pre></td></tr></table></figure><p>Instead, use the following approach:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">update xxx set amount=amount-<span class="number">1</span> where id=x and amount&gt;=<span class="number">1</span></span><br></pre></td></tr></table></figure><p>If the business logic allows negative balances, you can omit the amount validation in the WHERE clause. Otherwise, when the balance would go negative, you should skip the database update and return an exception in the application code.</p><h2 id="Rate-Limiting"><a href="#Rate-Limiting" class="headerlink" title="Rate Limiting"></a>Rate Limiting</h2><p>Since high-frequency accounts only cause system performance issues when a single account’s concurrency reaches a certain level, we can forcibly control this concurrency level to keep it within an acceptable range for the system.</p><h2 id="Caching"><a href="#Caching" class="headerlink" title="Caching"></a>Caching</h2><p>Caching is also a common approach for handling high-frequency accounts. Operations are performed on the balance in the cache, and the data is synchronized to the database periodically.</p><p>The above methods each have their applicable conditions and limitations. For example, merging and rate limiting both result in delayed deduction. During this delay period, the account balance might already be exhausted. So for business scenarios where the balance strictly cannot go negative and records cannot be discarded, these approaches may not be suitable.</p><p>In summary, the most important thing is to choose the appropriate solution based on the business scenario.</p><p>Source: <a href="https://lichuanyang.top/en/posts/56940/">https://lichuanyang.top/en/posts/56940/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/56940/</id>
    <link href="https://lichuanyang.top/en/posts/56940/"/>
    <published>2018-08-25T03:10:28.000Z</published>
    <summary>Practical experience with balance deduction under high concurrency scenarios, solving billing system issues caused by concurrent user operations on the same account.</summary>
    <title>Some Experience with Balance Deduction Under High Concurrency</title>
    <updated>2026-06-08T08:17:05.796Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <content>
      <![CDATA[<h2 id="Problem-Introduction"><a href="#Problem-Introduction" class="headerlink" title="Problem Introduction"></a>Problem Introduction</h2><p>I encountered an interesting problem today while scanning code with FindBugs — it’s about the ternary operator. Let me record it here.</p><span id="more"></span><p>It’s about code like this:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">boolean</span> <span class="variable">b</span> <span class="operator">=</span> <span class="literal">true</span>; </span><br><span class="line"><span class="type">Long</span> <span class="variable">a</span> <span class="operator">=</span> b ? <span class="number">0l</span> : Long.valueOf(<span class="number">2</span>); </span><br></pre></td></tr></table></figure><p>FindBugs gave the warning: “Boxed value is unboxed and then immediately reboxed” — meaning a boxed object is unboxed and then immediately reboxed. This problem is actually quite common. I didn’t pay much attention to it at first, and just habitually changed <code>Long.valueOf</code> to <code>Long.parseLong</code>, which did eliminate the warning. But later I realized something was wrong: <code>valueOf</code> returns a <code>Long</code> type while <code>parseLong</code> returns a <code>long</code> type, and what we need is actually the <code>Long</code> type. So why does using <code>valueOf</code> cause a problem while <code>parseLong</code> doesn’t?</p><p>If you think about it, it’s actually straightforward. The issue lies in the other branch of the ternary operator — because the other branch returns an unboxed <code>0</code>, the return type of this ternary operator becomes <code>long</code>, so the original <code>Long</code> type must go through an unboxing operation before being returned. To optimize this, just make sure both branches have consistent return types.</p><p>Then I looked up the related details and got a clearer understanding.</p><h2 id="Autoboxing-Unboxing"><a href="#Autoboxing-Unboxing" class="headerlink" title="Autoboxing&#x2F;Unboxing"></a>Autoboxing&#x2F;Unboxing</h2><p>Since JDK 1.5, Java has introduced autoboxing and unboxing, eliminating the need for explicit type conversions and improving our development efficiency. For example:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Double</span> <span class="variable">dWrap1</span> <span class="operator">=</span> <span class="number">10d</span>; </span><br><span class="line"></span><br><span class="line"><span class="type">double</span> <span class="variable">d1</span> <span class="operator">=</span> dWrap1; </span><br><span class="line"></span><br><span class="line"><span class="type">double</span> <span class="variable">d2</span> <span class="operator">=</span> d1 + dWrap1; </span><br><span class="line"></span><br><span class="line">DoubledWarp2 = d2 + dWrap1; </span><br></pre></td></tr></table></figure><p>This code can run normally.</p><p>Another thing to note is that in an expression involving type conversion — such as the ternary operator mentioned earlier — the compiler will prioritize primitive types, meaning it will first unbox already boxed objects.</p><p>Our original problem was just a minor performance overhead:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Long</span> <span class="variable">B</span> <span class="operator">=</span> <span class="literal">null</span>; </span><br><span class="line"><span class="type">Long</span> <span class="variable">A</span> <span class="operator">=</span> (<span class="number">2</span>&gt;<span class="number">1</span>)?B:<span class="number">0l</span>; </span><br></pre></td></tr></table></figure><p>But code like this is actually buggy. It looks like we’re just assigning a null to a <code>Long</code>-typed <code>A</code>, but during this process, an unboxing to <code>long</code> will occur, which will definitely cause a <code>NullPointerException</code>.</p><p>Therefore, in our daily development, we should try to avoid unnecessary boxing&#x2F;unboxing and type conversions. Not only for performance reasons, but also to avoid some strange issues.</p><p>Source: <a href="https://lichuanyang.top/en/posts/53072/">https://lichuanyang.top/en/posts/53072/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/53072/</id>
    <link href="https://lichuanyang.top/en/posts/53072/"/>
    <published>2018-05-10T13:22:36.000Z</published>
    <summary>A problem discovered through FindBugs, analyzing the autoboxing trap and boxing overhead in Java's ternary operator.</summary>
    <title>Java Details: Ternary Operator and Autoboxing</title>
    <updated>2026-06-08T07:50:49.093Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="open-source-project" scheme="https://lichuanyang.top/en/tags/open-source-project/"/>
    <category term="tools" scheme="https://lichuanyang.top/en/tags/tools/"/>
    <content>
      <![CDATA[<h2 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h2><p>I’ve been looking for an apartment recently, and I wanted to find a location that’s relatively convenient to get to several places. Checking the map manually was quite troublesome, so I decided to build a small tool for planning Beijing subway routes. This article briefly introduces the implementation process. The current functionality is relatively simple — the main method takes an input starting station and lists the routes with the minimum number of stops from all other stations to that location.</p><span id="more"></span><h2 id="Data-Acquisition"><a href="#Data-Acquisition" class="headerlink" title="Data Acquisition"></a>Data Acquisition</h2><p>I found the Amap (Gaode Map) API data online: <a href="http://map.amap.com/service/subway?_1469083453978&srhdata=1100_drw_beijing.json">http://map.amap.com/service/subway?_1469083453978&amp;srhdata=1100_drw_beijing.json</a> . I parsed the returned JSON string, which contains information about each subway line, listing each station along the line.</p><p>By parsing the data, the main information we need is about each station. The data structure for a station is defined as follows:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> String id;</span><br><span class="line"><span class="keyword">private</span> String name;</span><br><span class="line"><span class="keyword">private</span> Set&lt;String&gt; lines = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;String&gt;(); <span class="comment">//所在线路</span></span><br><span class="line"><span class="keyword">private</span> String position;</span><br><span class="line"><span class="keyword">private</span> String pinyin;</span><br><span class="line"><span class="keyword">private</span> Set&lt;String&gt; nextStations = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;String&gt;(); <span class="comment">//相邻站点</span></span><br></pre></td></tr></table></figure><p>After aggregating all stations, the entire network can be treated as a graph. The <code>nextStations</code> field is used to represent edge information.</p><h2 id="Route-Planning"><a href="#Route-Planning" class="headerlink" title="Route Planning"></a>Route Planning</h2><p>The route planning algorithm can reference Dijkstra’s algorithm. Since the current representation is just an undirected unweighted graph, the implementation is even simpler.</p><p>The implementation process is described as follows:</p><ol><li>Create two maps: <code>knownPath</code> and <code>waitingPath</code>, representing the list of stations whose shortest paths have been determined and the list of stations pending processing, respectively. During initialization, <code>knownPath</code> contains only the specified starting point, <code>waitingPath</code> contains all other stations, and the distance is set to infinity (represented by MAX &#x3D; 20000)</li><li>Set the starting point as the current node</li><li>Process each neighboring node of the current node sequentially, updating the shortest distance for each node</li><li>Find the node with the shortest distance from <code>waitingPath</code>, set it as the current node, repeat step 3, and move it from <code>waitingPath</code> to <code>knownPath</code></li></ol><p>After the entire process completes, you’ll get the shortest distance and route details from each node to the starting point, and then you can calculate transfer information for the route details.</p><p>For transfer detection, the main logic is: if the previous and next stations of a station are on different lines, we can determine that a transfer occurred at that station. For example, Dengshikou-Dongsi-Chaoyangmen: Dengshikou is on Line 5, Chaoyangmen is on Lines 2 and 6, so we can conclude that a transfer definitely occurred at Dongsi. However, there are some special cases to handle. For instance, Xizhimen to Ping’anli — if the middle station is Chegongzhuang (although no one would actually do this in reality), a transfer actually occurred, but the above logic would incorrectly determine it as no transfer.</p><p>The final route information contains the following:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> String stationId;</span><br><span class="line"><span class="keyword">private</span> <span class="type">int</span> length;</span><br><span class="line"><span class="keyword">private</span> <span class="type">int</span> transferNum; <span class="comment">//换乘数</span></span><br><span class="line"><span class="keyword">private</span> List&lt;String&gt; detail = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(); <span class="comment">//详细路径</span></span><br></pre></td></tr></table></figure><h2 id="Use-Cases"><a href="#Use-Cases" class="headerlink" title="Use Cases"></a>Use Cases</h2><p>The main functionality is already complete, and you can customize processing as needed. For example, if I need to find “locations within 15 stops of Olympic Green Station and within 7 stops of Tian’anmen East Station,” I can input Olympic Green and Tian’anmen East separately, then filter and intersect the results accordingly.</p><h2 id="Future-Plans"><a href="#Future-Plans" class="headerlink" title="Future Plans"></a>Future Plans</h2><p>The current implementation is relatively simple, only considering the number of stops. In reality, the distance and travel time between different stations can vary significantly, and the transfer cost at different locations also varies greatly. So in the future, I’ll try to find detailed transfer station data and travel time data between stations to apply to the algorithm.</p><p>Another thing is to find a suitable method to build a UI, since everything has been purely backend so far, and I haven’t decided what would be appropriate yet.</p><p>Additionally, I’m considering scraping housing price information.</p><h2 id="Code"><a href="#Code" class="headerlink" title="Code"></a>Code</h2><p><a href="https://github.com/lcy362/FoxSubway">https://github.com/lcy362/FoxSubway</a></p><p>Feedback is welcome.</p><p>Source: <a href="https://lichuanyang.top/en/posts/13793/">https://lichuanyang.top/en/posts/13793/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/13793/</id>
    <link href="https://lichuanyang.top/en/posts/13793/"/>
    <published>2018-03-23T06:33:52.000Z</published>
    <summary>A Java implementation of a Beijing subway route planning tool that fetches data from the Amap (Gaode) API and calculates routes with the minimum number of stops.</summary>
    <title>Designing an Open-Source Beijing Subway Route Planner (Java Version)</title>
    <updated>2026-06-27T03:53:05.195Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Redis Series" scheme="https://lichuanyang.top/en/categories/Redis-Series/"/>
    <category term="redis" scheme="https://lichuanyang.top/en/tags/redis/"/>
    <category term="redis-cluster" scheme="https://lichuanyang.top/en/tags/redis-cluster/"/>
    <content>
      <![CDATA[<p>In a previous article <a href="https://lichuanyang.top/posts/9329/">Understanding Redis Cluster Principles Through Hands-on Practice</a>, we briefly introduced the design principles of Redis Cluster. Data in Redis Cluster is distributed across 16384 slots according to certain rules, and these slots are mapped to different nodes based on the configuration. We know that after the cluster is running stably, data can still be transferred at the slot level. However, I’ve never been entirely sure about the specific transfer process, including cluster availability during migration. So this time I looked into it in detail.</p><span id="more"></span><h2 id="Overall-Process"><a href="#Overall-Process" class="headerlink" title="Overall Process"></a>Overall Process</h2><p>The data migration method provided in the official Redis documentation uses the redis-trib script. Strictly speaking, redis-trib is not part of Redis itself—it’s just a set of scripts implemented by the official team according to Redis design specifications, to help users use Redis Cluster more conveniently. In fact, we can completely use the cluster without this script, or implement the same logic in other ways. For example, Sohu TV’s Redis operations tool CacheCloud implements the entire logic in Java.</p><p>We can refer to the redis-trib or CacheCloud source code to understand the cluster data migration process, which mainly consists of the following steps:</p><ol><li>Set the node states for migration. For example, to migrate data of slot x from Node A to Node B, we need to set A to MIGRATING state and B to IMPORTING state. <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CLUSTER SETSLOT &lt;slot&gt; IMPORTING &lt;node_id&gt;</span><br></pre></td></tr></table></figure> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CLUSTER SETSLOT &lt;slot&gt; MIGRATING &lt;node_id&gt;</span><br></pre></td></tr></table></figure></li><li>Migrate data. This step first uses the CLUSTER GETKEYSINSLOT command to get all keys in that slot, then transfers each key’s data using the MIGRATE command one by one.</li><li>After the data transfer is complete, officially assign the slot to the new Node B. <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CLUSTER SETSLOT &lt;slot&gt; NODE &lt;node_id&gt;</span><br></pre></td></tr></table></figure></li></ol><h2 id="Availability"><a href="#Availability" class="headerlink" title="Availability"></a>Availability</h2><p>During the entire migration process, there will be blocking for individual keys. This is because the MIGRATE command is atomic—during the migration of a single key, access to that key will be blocked. However, in general, a single key’s data won’t be particularly large, so in most cases it completes instantly and generally won’t actually affect usage. Any other situation won’t cause cluster unavailability. If it does occur—for example, slot-level unavailability—it indicates there are some issues with the client-side handling. This article will also cover some client-side usage considerations.</p><h2 id="ASK-and-MOVED-Redirections"><a href="#ASK-and-MOVED-Redirections" class="headerlink" title="ASK and MOVED Redirections"></a>ASK and MOVED Redirections</h2><p>As mentioned earlier, Redis Cluster data migration basically won’t affect cluster usage. However, during the data transfer from Node A to Node B, the data might be on A or on B. How does Redis know which node to look for? This is where we need to introduce the ASK and MOVED redirection signals. As the names suggest, this information means the data needed is not on the current node and requires a redirection. MOVED is a permanent redirection signal, while ASK indicates that only this particular operation needs redirection.</p><p>For example, during the data migration from Node A to Node B, various keys are scattered across both nodes. So when the client doesn’t find a certain key on A, it receives an ASK redirection and then goes to look on B—essentially requiring one extra lookup.</p><p>It’s worth noting that when querying B, the client needs to first send an ASKING command; otherwise, the request targeting a slot in IMPORTING state will be rejected by Node B.</p><p>For the client, simply put: when receiving MOVED, it needs to update the slot mapping information; when receiving ASK, it needs to send an ASKING command to the new node and re-execute the operation.</p><p>Looking at the Jedis source code, it implements exactly this logic:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">&#125; <span class="keyword">catch</span> (JedisRedirectionException jre) &#123;</span><br><span class="line">      <span class="comment">// if MOVED redirection occurred,</span></span><br><span class="line">      <span class="keyword">if</span> (jre <span class="keyword">instanceof</span> JedisMovedDataException) &#123;</span><br><span class="line">        <span class="comment">// it rebuilds cluster&#x27;s slot cache</span></span><br><span class="line">        <span class="comment">// recommended by Redis cluster specification</span></span><br><span class="line">        <span class="built_in">this</span>.connectionHandler.renewSlotCache(connection);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// release current connection before recursion or renewing</span></span><br><span class="line">      releaseConnection(connection);</span><br><span class="line">      connection = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (jre <span class="keyword">instanceof</span> JedisAskDataException) &#123;</span><br><span class="line">        asking = <span class="literal">true</span>;</span><br><span class="line">        askConnection.set(<span class="built_in">this</span>.connectionHandler.getConnectionFromNode(jre.getTargetNode()));</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (jre <span class="keyword">instanceof</span> JedisMovedDataException) &#123;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">JedisClusterOperationException</span>(jre);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">return</span> runWithRetries(slot, attempts - <span class="number">1</span>, <span class="literal">false</span>, asking);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br></pre></td></tr></table></figure><p>When an exception occurs during operation, it separately checks for MovedException and AskException, then handles them accordingly.</p><p>Source: <a href="https://lichuanyang.top/en/posts/37583/">https://lichuanyang.top/en/posts/37583/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/37583/</id>
    <link href="https://lichuanyang.top/en/posts/37583/"/>
    <published>2018-02-24T10:34:19.000Z</published>
    <summary>Detailed process of Redis Cluster data migration, including the slot transfer mechanism and cluster availability analysis during migration.</summary>
    <title>Redis Cluster Data Migration</title>
    <updated>2026-06-27T03:50:56.861Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="JStorm Source Code Analysis" scheme="https://lichuanyang.top/en/categories/JStorm-Source-Code-Analysis/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/en/tags/jstorm/"/>
    <content>
      <![CDATA[<h2 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h2><p>AsyncLoopThread is a custom loop task execution utility in JStorm. The implementation is not complex, and on its own may not warrant a dedicated article. However, it is so widely used in JStorm — for features such as supervisor&#x2F;nimbus heartbeat, fetching new topologies, updating worker status, and many more — that it’s worth introducing here, as it will also help with reading other parts of the codebase.</p><span id="more"></span><h2 id="Usage"><a href="#Usage" class="headerlink" title="Usage"></a>Usage</h2><p>AsyncLoopThread can actually be extracted and used in your own projects. It’s quite convenient to use. You can refer to <a href="https://github.com/lcy362/Scenes/blob/4d8ec4ff166060cf5d491c33a96f5c86e8389333/src/main/java/com/mallow/jstormcode/AsyncLoop.java">https://github.com/lcy362/Scenes/blob/4d8ec4ff166060cf5d491c33a96f5c86e8389333/src/main/java/com/mallow/jstormcode/AsyncLoop.java</a></p><p>You only need to first define a RunnableCallback class. RunnableCallback is a thread class wrapped by JStorm that enhances the Runnable interface with callback support and the ability to proactively shut down.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestThread</span> <span class="keyword">extends</span> <span class="title class_">RunnableCallback</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;thread runs &quot;</span> + <span class="keyword">new</span> <span class="title class_">Date</span>());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">getResult</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">10</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Here we only implement two methods: run and getResult. The run method is the thread execution method, same as a normal thread. getResult returns the task execution interval time, in seconds.</p><p>After that, you just need to pass this TestThread as a parameter to AsyncLoopThread, and it will implement the loop execution of TestThread’s run method.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String args[])</span> &#123;</span><br><span class="line">        <span class="type">AsyncLoopThread</span> <span class="variable">loop</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AsyncLoopThread</span>(<span class="keyword">new</span> <span class="title class_">TestThread</span>());</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            TimeUnit.MINUTES.sleep(<span class="number">5</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Design"><a href="#Design" class="headerlink" title="Design"></a>Design</h2><p>Next, let’s look at the specific implementation of AsyncLoopThread.</p><p>The core code is the init method:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(RunnableCallback afn, <span class="type">boolean</span> daemon, RunnableCallback kill_fn, <span class="type">int</span> priority, <span class="type">boolean</span> start)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (kill_fn == <span class="literal">null</span>) &#123;</span><br><span class="line">        kill_fn = <span class="keyword">new</span> <span class="title class_">AsyncLoopDefaultKill</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">Runnable</span> <span class="variable">runnable</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AsyncLoopRunnable</span>(afn, kill_fn);</span><br><span class="line">    thread = <span class="keyword">new</span> <span class="title class_">Thread</span>(runnable);</span><br><span class="line">    <span class="type">String</span> <span class="variable">threadName</span> <span class="operator">=</span> afn.getThreadName();</span><br><span class="line">    <span class="keyword">if</span> (threadName == <span class="literal">null</span>) &#123;</span><br><span class="line">        threadName = afn.getClass().getSimpleName();</span><br><span class="line">    &#125;</span><br><span class="line">    thread.setName(threadName);</span><br><span class="line">    thread.setDaemon(daemon);</span><br><span class="line">    thread.setPriority(priority);</span><br><span class="line">    thread.setUncaughtExceptionHandler(<span class="keyword">new</span> <span class="title class_">UncaughtExceptionHandler</span>() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">uncaughtException</span><span class="params">(Thread t, Throwable e)</span> &#123;</span><br><span class="line">            LOG.error(<span class="string">&quot;UncaughtException&quot;</span>, e);</span><br><span class="line">            JStormUtils.halt_process(<span class="number">1</span>, <span class="string">&quot;UncaughtException&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.afn = afn;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (start) &#123;</span><br><span class="line">        thread.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The main thing here is the creation of a new AsyncLoopRunnable class, where the more core code resides. Additionally, note the kill_fn parameter, which is used in AsyncLoopRunnable to kill the task. The thread is also configured with several settings: priority, exception handling, etc. Furthermore, all threads here are set as daemon threads, which is why in the example above, we need to explicitly make the main thread sleep to observe the running effect.</p><p>Next, let’s look at AsyncLoopRunnable:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (fn == <span class="literal">null</span>) &#123;</span><br><span class="line">        LOG.error(<span class="string">&quot;fn==null&quot;</span>);</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;AsyncLoopRunnable no core function &quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fn.preRun();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (!shutdown.get()) &#123;</span><br><span class="line">            fn.run();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (shutdown.get()) &#123;</span><br><span class="line">                shutdown();</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">Exception</span> <span class="variable">e</span> <span class="operator">=</span> fn.error();</span><br><span class="line">            <span class="keyword">if</span> (e != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> e;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">Object</span> <span class="variable">rtn</span> <span class="operator">=</span> fn.getResult();</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.needQuit(rtn)) &#123;</span><br><span class="line">                shutdown();</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Throwable e) &#123;</span><br><span class="line">        <span class="keyword">if</span> (shutdown.get()) &#123;</span><br><span class="line">            shutdown();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            LOG.error(<span class="string">&quot;Async loop died!!!&quot;</span> + e.getMessage(), e);</span><br><span class="line">            killFn.execute(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This code is also quite simple — it loops through the execution of RunnableCallback’s run method. During execution, there are several scenarios that cause the task to be interrupted or the process to be killed directly: shutdown, needQuit(), and exceptions. Among these, the needQuit() method controls the task execution speed based on the getResult return value mentioned earlier.</p><p>Original article: <a href="https://lichuanyang.top/posts/31761">https://lichuanyang.top/posts/31761</a></p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/31761/">https://lichuanyang.top/en/posts/31761/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/31761/</id>
    <link href="https://lichuanyang.top/en/posts/31761/"/>
    <published>2018-02-08T08:17:37.000Z</published>
    <summary>Source code analysis of JStorm's custom loop task utility AsyncLoopThread, understanding the underlying implementation of supervisor/nimbus heartbeat and other functions.</summary>
    <title>JStorm Source Code Analysis: The Loop Task AsyncLoopThread</title>
    <updated>2026-06-27T03:49:49.959Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="ActiveMQ Series" scheme="https://lichuanyang.top/en/categories/ActiveMQ-Series/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>Data persistence is a concern that many systems deal with, especially for systems like Redis and ActiveMQ where data is primarily stored in memory. Since data is stored in memory, there is a risk of data loss when the system crashes. The solution to this problem is to write data to disk through some mechanism — that is, <strong>persistence</strong>.</p><p>ActiveMQ provides three persistence methods, based on JDBC, KahaDB, and LevelDB respectively. Currently, the officially recommended method is <strong>KahaDB-based persistence</strong>. <span id="more"></span> JDBC was the earliest persistence method provided by ActiveMQ, but using a database for persistence is not really appropriate. After all, there are performance bottlenecks, and what is needed is simply read&#x2F;write data storage without requiring all the powerful features of a database. If you look at the documentation now, even the basic configuration is buried deep, so we will not go into detail about this method.</p><p>Precisely because of the various problems with JDBC-based persistence, ActiveMQ subsequently introduced persistence methods based on KahaDB and LevelDB. <a href="https://github.com/google/leveldb">LevelDB</a> is a key-value disk storage system open-sourced by Google, widely used in many applications. KahaDB’s origin is unclear — it was likely developed by the ActiveMQ team and is also a disk-based storage system. In theory, LevelDB should have better performance. Previously, whether in ActiveMQ’s default configuration or the recommended usage in the documentation, LevelDB was the preferred choice. However, one day, the LevelDB-based persistence method was suddenly deprecated by ActiveMQ. The main reason is that LevelDB is a third-party system, and maintaining it is not as convenient as KahaDB. In the current latest version, LevelDB persistence still has many serious issues and its functionality is not as complete as KahaDB. Therefore, the most recommended persistence method currently is <strong>KahaDB</strong>. Let’s look at the basic configuration below.</p><h2 id="Basic-Configuration"><a href="#Basic-Configuration" class="headerlink" title="Basic Configuration"></a>Basic Configuration</h2><p>The basic configuration is very simple — just look at the default configuration. Open <code>activemq.xml</code>.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;persistenceAdapter&gt;</span><br><span class="line">    &lt;kahaDB directory=&quot;$&#123;activemq.data&#125;/kahadb&quot;/&gt;</span><br><span class="line">&lt;/persistenceAdapter&gt;</span><br></pre></td></tr></table></figure><p>This way, data will be automatically synced to the KahaDB directory. You can look at the file structure in the directory. The main storage files are a series of <code>.log</code> files. Each piece of data that needs to be persisted is written sequentially. When a log file reaches a certain size, a new one is created. When all messages in a file have been consumed, that file will be deleted.</p><h2 id="Parameters"><a href="#Parameters" class="headerlink" title="Parameters"></a>Parameters</h2><p>You can refer to the documentation for parameters: <a href="http://activemq.apache.org/kahadb.html">http://activemq.apache.org/kahadb.html</a>.</p><p>Here are a few useful ones:</p><ul><li><code>cleanupInterval</code> — The interval for periodically checking which files need to be cleaned up. Default is 30 seconds.</li><li><code>journalMaxFileLength</code> — The maximum size of each log file. Default is 32MB.</li><li><code>journalDiskSyncInterval</code>, <code>journalDiskSyncStrategy</code>, <code>journalMaxFileLength</code> — Parameters related to asynchronous disk writing. Asynchronous disk writing can improve efficiency, but may result in data loss.</li></ul><h2 id="Usage-Tips"><a href="#Usage-Tips" class="headerlink" title="Usage Tips"></a>Usage Tips</h2><p>Here are a few useful tips. First, regarding logging: you can enable TRACE-level logging for KahaDB by adding a logger configuration for <code>org.apache.activemq.store.kahadb.MessageDatabase</code> in log4j. For example:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">log4j.appender.kahadb=org.apache.log4j.RollingFileAppender</span><br><span class="line">log4j.appender.kahadb.file=$&#123;activemq.base&#125;/data/kahadb.log</span><br><span class="line">log4j.appender.kahadb.maxFileSize=1024KB</span><br><span class="line">log4j.appender.kahadb.maxBackupIndex=5</span><br><span class="line">log4j.appender.kahadb.append=true</span><br><span class="line">log4j.appender.kahadb.layout=org.apache.log4j.PatternLayout</span><br><span class="line">log4j.appender.kahadb.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c&#123;1&#125; - %m%n</span><br><span class="line">log4j.logger.org.apache.activemq.store.kahadb.MessageDatabase=TRACE, kahadb</span><br></pre></td></tr></table></figure><p>Sometimes you may encounter issues where KahaDB files cannot be deleted. Looking at it directly may not reveal which queues are causing the problem. This log will print out which files were cleaned up, and why other files were not cleaned up, among other key information.</p><p>Another tip is that you can configure separate storage directories for each queue:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;persistenceAdapter&gt;</span><br><span class="line">  &lt;mKahaDB directory=&quot;$&#123;activemq.base&#125;/data/kahadb&quot;&gt;</span><br><span class="line">    &lt;filteredPersistenceAdapters&gt;</span><br><span class="line">      &lt;!-- kahaDB per destinations --&gt;</span><br><span class="line">      &lt;filteredKahaDB perDestination=&quot;true&quot;&gt;</span><br><span class="line">        &lt;persistenceAdapter&gt;</span><br><span class="line">          &lt;kahaDB journalMaxFileLength=&quot;32mb&quot;/&gt;</span><br><span class="line">        &lt;/persistenceAdapter&gt;</span><br><span class="line">      &lt;/filteredKahaDB&gt;</span><br><span class="line">    &lt;/filteredPersistenceAdapters&gt;</span><br><span class="line">  &lt;/mKahaDB&gt;</span><br><span class="line"> &lt;/persistenceAdapter&gt;</span><br></pre></td></tr></table></figure><p>The reason for doing this is related to storage space. KahaDB writes files sequentially by message order, and files can only be deleted when all messages in that file have been consumed. This means that even if there is only one unconsumed message in a file, the entire file’s space must still be occupied. If there are many queues and one queue’s consumption is lagging behind, it may occupy very little space itself but still hold a large amount of disk space that cannot be released. Configuring separate directories for each queue can significantly alleviate this situation.</p><h2 id="Source-https-lichuanyang-top-en-posts-31044"><a href="#Source-https-lichuanyang-top-en-posts-31044" class="headerlink" title="Source: https://lichuanyang.top/en/posts/31044/"></a>Source: <a href="https://lichuanyang.top/en/posts/31044/">https://lichuanyang.top/en/posts/31044/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/31044/</id>
    <link href="https://lichuanyang.top/en/posts/31044/"/>
    <published>2018-02-02T10:56:49.000Z</published>
    <summary>An in-depth introduction to the three persistence methods in ActiveMQ (JDBC, KahaDB, LevelDB), focusing on the officially recommended KahaDB persistence mechanism.</summary>
    <title>ActiveMQ Feature - Persistence</title>
    <updated>2026-06-27T03:50:56.859Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="ActiveMQ Series" scheme="https://lichuanyang.top/en/categories/ActiveMQ-Series/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <content>
      <![CDATA[<p>ActiveMQ is an open-source messaging middleware (Message-Oriented Middleware, MOM) under the Apache Software Foundation and a complete implementation of the JMS (Java Message Service) 1.1 specification. This article is the opening piece of the ActiveMQ series. We’ll first clarify what messaging middleware is and what problems it solves, then introduce the core concepts of JMS, laying a solid foundation for further exploration of ActiveMQ’s specific features.</p><span id="more"></span><h2 id="What-Problems-Does-Messaging-Middleware-Solve"><a href="#What-Problems-Does-Messaging-Middleware-Solve" class="headerlink" title="What Problems Does Messaging Middleware Solve?"></a>What Problems Does Messaging Middleware Solve?</h2><p>Messaging middleware is a very common component in distributed systems. Its core role is to act as an intermediary between applications — applications do not communicate directly with each other, but pass messages through the messaging middleware instead.</p><p>There are two primary values it delivers: <strong>decoupling</strong> and <strong>peak shaving</strong>.</p><h3 id="Decoupling"><a href="#Decoupling" class="headerlink" title="Decoupling"></a>Decoupling</h3><p>Imagine a user login scenario: after a successful login, multiple downstream systems might need to respond — sending a welcome message to the user, updating a recommendation algorithm, syncing user data to a data warehouse, triggering risk control checks… These operations have nothing to do with the login itself. If all of this logic were crammed into the login module, not only would the code become a tangled mess, but the login response time would also become unacceptable.</p><p>By introducing messaging middleware, the login module only needs to publish a “user logged in” message after successful authentication. Other systems each listen for this message and execute their own business logic independently. The login module no longer needs to know which downstream systems exist, and adding a new consumer does not require modifying any login code.</p><h3 id="Peak-Shaving"><a href="#Peak-Shaving" class="headerlink" title="Peak Shaving"></a>Peak Shaving</h3><p>System resources are allocated based on routine traffic, not provisioned for flash-sale-level peaks. When instantaneous traffic far exceeds the system’s processing capacity, rather than letting requests hit the database directly and bring the entire system down, messages can queue up in the middleware first, with downstream systems consuming them at their own pace.</p><p>For example: during a flash sale, 100,000 order requests flood in within one second, but the database can only process 1,000 per second. Using messaging middleware as a buffer, requests are first written into message queues, and backend services process them at a steady 1,000 QPS, ensuring the system is not overwhelmed.</p><h2 id="Two-Message-Delivery-Modes"><a href="#Two-Message-Delivery-Modes" class="headerlink" title="Two Message Delivery Modes"></a>Two Message Delivery Modes</h2><p>Messaging middleware generally supports two message delivery modes:</p><h3 id="Point-to-Point-Mode-P2P-Queue"><a href="#Point-to-Point-Mode-P2P-Queue" class="headerlink" title="Point-to-Point Mode (P2P &#x2F; Queue)"></a>Point-to-Point Mode (P2P &#x2F; Queue)</h3><p>In JMS, this corresponds to <strong>Queue</strong>, with three characteristics:</p><ol><li><strong>One message, one consumer</strong>: multiple consumers can listen on the same Queue, but each message will be consumed by only one of them.</li><li><strong>Temporal decoupling</strong>: the receiver does not need to be online when the sender sends a message. Messages are persisted in the queue, waiting for the receiver to come online and consume them.</li><li><strong>Acknowledgment required</strong>: after successfully processing a message, the consumer needs to send an acknowledgment (ACK) to the queue before the message is marked as consumed.</li></ol><p>Typical scenarios: order processing, asynchronous task distribution.</p><h3 id="Publish-Subscribe-Mode-Pub-Sub-Topic"><a href="#Publish-Subscribe-Mode-Pub-Sub-Topic" class="headerlink" title="Publish-Subscribe Mode (Pub&#x2F;Sub &#x2F; Topic)"></a>Publish-Subscribe Mode (Pub&#x2F;Sub &#x2F; Topic)</h3><p>In JMS, this corresponds to <strong>Topic</strong>:</p><ol><li><strong>One message, multiple consumers</strong>: once a publisher sends a message, all consumers subscribed to that Topic will receive it.</li><li><strong>Temporal coupling</strong>: subscribers must remain online to receive messages (unless using Durable Subscription).</li><li><strong>Broadcast characteristics</strong>: well-suited for one-to-many notification scenarios.</li></ol><p>Typical scenarios: configuration change notifications, real-time push, event broadcasting.</p><h3 id="Queue-vs-Topic-Comparison"><a href="#Queue-vs-Topic-Comparison" class="headerlink" title="Queue vs Topic Comparison"></a>Queue vs Topic Comparison</h3><table><thead><tr><th>Feature</th><th>Queue</th><th>Topic</th></tr></thead><tbody><tr><td>Consumption model</td><td>One message, one consumer</td><td>One message broadcast to all subscribers</td></tr><tr><td>Temporal dependency</td><td>None (messages are persisted)</td><td>Yes (subscribers must be online, except for durable subscriptions)</td></tr><tr><td>Message acknowledgment</td><td>ACK required</td><td>Fire and forget</td></tr><tr><td>Typical scenarios</td><td>Async tasks, peak shaving</td><td>Event notifications, real-time push</td></tr></tbody></table><h2 id="What-is-JMS"><a href="#What-is-JMS" class="headerlink" title="What is JMS?"></a>What is JMS?</h2><p>JMS (Java Message Service) is a set of messaging API specifications defined by Sun. JMS itself is not a concrete messaging system — it only defines the abstractions of interfaces and classes needed for messaging clients to communicate with messaging systems, similar to how JDBC defines database access interfaces and JNDI defines naming and directory service interfaces.</p><p>The JMS specification primarily defines:</p><ul><li><strong>Message structure</strong>: message header, properties, and body</li><li><strong>Connection factory</strong>: <code>ConnectionFactory</code>, used to create connections to the messaging middleware</li><li><strong>Connection and session</strong>: <code>Connection</code> and <code>Session</code>, managing the sending and receiving of messages</li><li><strong>Destinations</strong>: <code>Queue</code> and <code>Topic</code></li><li><strong>Producers and consumers</strong>: <code>MessageProducer</code> and <code>MessageConsumer</code></li></ul><p>ActiveMQ is a complete implementation of the JMS 1.1 specification.</p><h2 id="ActiveMQ’s-Positioning"><a href="#ActiveMQ’s-Positioning" class="headerlink" title="ActiveMQ’s Positioning"></a>ActiveMQ’s Positioning</h2><p>In the messaging middleware ecosystem, ActiveMQ’s unique strengths include:</p><ul><li><strong>Best JMS compatibility</strong>: full JMS 1.1 support, making it the best practice reference for learning the JMS specification</li><li><strong>Multi-protocol support</strong>: supports various transport protocols including OpenWire, AMQP, MQTT, and STOMP</li><li><strong>Flexible persistence</strong>: supports multiple persistence methods such as JDBC, KahaDB, and LevelDB</li><li><strong>Rich clustering solutions</strong>: Master-Slave, Network of Brokers, and more</li></ul><p>In subsequent articles in this series, we will dive deeper into ActiveMQ installation and configuration, persistence mechanisms, cluster deployment, plugin development, and other topics.</p><h2 id="Series-Navigation"><a href="#Series-Navigation" class="headerlink" title="Series Navigation"></a>Series Navigation</h2><blockquote><p>This is Article 1 of the ActiveMQ Series. Upcoming articles include:</p><ul><li><a href="">ActiveMQ Installation and Basic Configuration</a></li><li><a href="">ActiveMQ Persistence Mechanisms in Detail</a></li><li><a href="">ActiveMQ Multi-threaded Consumption Model</a></li><li><a href="/posts/61645/">ActiveMQ Plugin Development Guide</a></li><li><a href="/posts/32479/">ActiveMQ Web Console Permission Configuration</a></li></ul></blockquote><h2 id="Stay-tuned"><a href="#Stay-tuned" class="headerlink" title="Stay tuned."></a>Stay tuned.</h2>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/12035/</id>
    <link href="https://lichuanyang.top/en/posts/12035/"/>
    <published>2018-01-22T10:33:02.000Z</published>
    <summary>The opening article of the ActiveMQ messaging middleware series, introducing the core functions of messaging middleware (decoupling and peak shaving) as well as the basic concepts of the JMS protocol.</summary>
    <title>ActiveMQ Series - Overview</title>
    <updated>2026-06-27T02:18:02.585Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="monitoring" scheme="https://lichuanyang.top/en/tags/monitoring/"/>
    <category term="storm" scheme="https://lichuanyang.top/en/tags/storm/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/en/tags/jstorm/"/>
    <content>
      <![CDATA[<h2 id="JStorm-Metrics-System"><a href="#JStorm-Metrics-System" class="headerlink" title="JStorm Metrics System"></a>JStorm Metrics System</h2><p>JStorm’s UI provides a large number of very detailed monitoring parameters, which are extremely helpful for troubleshooting. For information about the UI, you can refer to my previous article: <a href="https://lichuanyang.top/posts/31996/">https://lichuanyang.top/posts/31996/</a>. However, using the UI can sometimes be inconvenient, for example when you need to query historical data. Therefore, we want to export monitoring data to other storage media for easier subsequent querying and analysis.</p><p>Since JStorm’s monitoring has been completely rewritten compared to Apache Storm, the monitoring export methods found online for Storm are not applicable to JStorm. And apart from the official documentation, JStorm lacks resources. The official documentation is too brief, providing only some hints, and you need to combine these hints with source code to understand the details. So I’ve put together an example of exporting JStorm monitoring data.</p><h2 id="Custom-Metrics-Upload"><a href="#Custom-Metrics-Upload" class="headerlink" title="Custom Metrics Upload"></a>Custom Metrics Upload</h2><p>First, you need to implement the MetricUploader interface. However, we don’t actually use any of the methods in this interface. The main purpose is to use its TopologyMetricsRunnable parameter, and then use this parameter to retrieve monitoring information. So theoretically, as long as you obtain TopologyMetricsRunnable, you don’t necessarily have to implement the MetricUploader interface. My approach is to implement MetricUploader, and then start a scheduled thread pool to periodically fetch monitoring data.</p><p>JStorm’s metric data is stored in RocksDB. The data retrieved here is essentially querying RocksDB using JStorm’s wrapped interfaces.</p><h2 id="Core-Code-Implementation"><a href="#Core-Code-Implementation" class="headerlink" title="Core Code Implementation"></a>Core Code Implementation</h2><p>The specific code is as follows:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ClusterSummary</span> <span class="variable">clusterInfo</span> <span class="operator">=</span> client.getClient().getClusterInfo();</span><br><span class="line"><span class="comment">//get list of topologies in this cluster</span></span><br><span class="line">List&lt;TopologySummary&gt; topologies = clusterInfo.get_topologies();</span><br><span class="line"><span class="keyword">for</span> (TopologySummary topology : topologies) &#123;</span><br><span class="line">    <span class="comment">//get topology id and name</span></span><br><span class="line">    <span class="comment">//the id is used for query, name for human reading</span></span><br><span class="line">    logger.info(<span class="string">&quot;topology info &quot;</span> + topology.get_id() + <span class="string">&quot; &quot;</span> + topology.get_name());</span><br><span class="line">    <span class="type">TopologyMetric</span> <span class="variable">metric</span> <span class="operator">=</span> metricsRunnable.getTopologyMetric(topology.get_id());</span><br><span class="line">    <span class="comment">//get data of &quot;component metrics&quot; page in jstorm UI</span></span><br><span class="line">    <span class="type">MetricInfo</span> <span class="variable">componentMetric</span> <span class="operator">=</span> metric.get_componentMetric();</span><br><span class="line">    Map&lt;String, Map&lt;Integer, MetricSnapshot&gt;&gt; metrics = componentMetric.get_metrics();</span><br><span class="line">    <span class="keyword">for</span> (Map.Entry&lt;String, Map&lt;Integer, MetricSnapshot&gt;&gt; oneMetric : metrics.entrySet()) &#123;</span><br><span class="line">        String[] key = oneMetric.getKey().split(<span class="string">&quot;@&quot;</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">metricKey</span> <span class="operator">=</span> key[<span class="number">1</span>] + <span class="string">&quot;@&quot;</span> + key[<span class="number">2</span>] + <span class="string">&quot;@&quot;</span> + key[<span class="number">6</span>];</span><br><span class="line">        <span class="comment">//get(60) to get data in 1 min, also can get(600) for 10min, and so on</span></span><br><span class="line">        logger.info(<span class="string">&quot;metric one minute data for &quot;</span> + metricKey + <span class="string">&quot; &quot;</span> + oneMetric.getValue().get(<span class="number">60</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Data-Parsing-and-Output"><a href="#Data-Parsing-and-Output" class="headerlink" title="Data Parsing and Output"></a>Data Parsing and Output</h2><p>The entire flow is quite clear. First, query the list of topologies in the cluster, then use each topology ID to query metric information, obtaining a TopologyMetric object. TopologyMetric contains topologyMetric, componentMetric, workerMetric, and other attributes, which correspond to the respective pages in the UI.</p><p>Taking componentMetric as an example, you can use componentMetric.get_metrics() to get the specific monitoring metric data. A metric is a <code>Map&lt;String, Map&lt;Integer, MetricSnapshot&gt;&gt;</code>, where the key is a string separated by ‘@’ characters, containing key information such as topology name, component name, and data item. The value of this map is a time (in seconds), corresponding to the 1-minute, 2-minute pages on the UI, and the value is the specific monitoring data. This data is actually richer than what the UI displays — besides averages, it also includes the 95th percentile, 99th percentile, etc.</p><p>In this example, I only output some of the data using logging. In practice, you can use storage media such as HBase, Redis, MySQL, etc. according to your needs.</p><p>The specific code can be viewed at <a href="https://github.com/lcy362/StormTrooper/blob/master/src/main/java/com/trooper/storm/monitor/MetricUploaderTest.java">https://github.com/lcy362/StormTrooper/blob/master/src/main/java/com/trooper/storm/monitor/MetricUploaderTest.java</a></p><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><ol><li><strong>Implement MetricUploader</strong>: Create a custom class implementing the <code>MetricUploader</code> interface. Use the <code>TopologyMetricsRunnable</code> object obtained via the constructor to query metrics from RocksDB.</li><li><strong>Write metrics collection logic</strong>: Call <code>metricsRunnable.getTopologyMetric(topologyId)</code> to get the <code>TopologyMetric</code>. Read <code>componentMetric</code>, <code>workerMetric</code>, and <code>topologyMetric</code> data, parse the <code>@</code>-delimited metric keys, and store them.</li><li><strong>Configure JStorm</strong>: Register your custom MetricUploader implementation in the JStorm config file to enable automatic metrics reporting at topology startup.</li><li><strong>Test</strong>: Start a topology and check whether metrics data is written to your target storage (HBase&#x2F;Redis&#x2F;MySQL, etc.). Cross-reference with the JStorm UI to ensure consistency.</li><li><strong>Connect Grafana</strong>: Configure the storage medium as a Grafana data source, write queries to build monitoring dashboards for historical data visualization and alerting.</li></ol><hr><p>Source: <a href="https://lichuanyang.top/en/posts/13749/">https://lichuanyang.top/en/posts/13749/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/13749/</id>
    <link href="https://lichuanyang.top/en/posts/13749/"/>
    <published>2017-09-06T12:18:00.000Z</published>
    <summary>A solution for exporting JStorm monitoring metrics data to third-party storage for convenient historical data querying and analysis.</summary>
    <title>Exporting JStorm Monitoring Metrics to Third-Party Storage</title>
    <updated>2026-06-27T02:41:48.957Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="camel" scheme="https://lichuanyang.top/en/tags/camel/"/>
    <content>
      <![CDATA[<p>Apache Camel is a powerful enterprise integration framework built on EIP (Enterprise Integration Patterns), with over 200 components that can easily connect various data sources and middleware. However, this power comes with a downside: the high level of encapsulation makes the underlying details opaque, and troubleshooting route problems can be very difficult.</p><p>Fortunately, the Camel team provides a Debugger tool that allows you to set breakpoints at route nodes and inspect message content, just like debugging a regular Java program. This article supplements the <a href="http://camel.apache.org/debugger.html">official documentation</a> with some practical debugging tips.</p><span id="more"></span><h2 id="Why-Use-the-Debugger"><a href="#Why-Use-the-Debugger" class="headerlink" title="Why Use the Debugger?"></a>Why Use the Debugger?</h2><p>Camel route definitions are typically very declarative, for example:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">from(<span class="string">&quot;file:inbox&quot;</span>)</span><br><span class="line">    .unmarshal().json()</span><br><span class="line">    .bean(SomeTransformer.class)</span><br><span class="line">    .to(<span class="string">&quot;activemq:queue:orders&quot;</span>);</span><br></pre></td></tr></table></figure><p>It looks clean and simple, but if a message gets processed incorrectly at some stage, how would you know whether the <code>unmarshal</code> step is at fault or if <code>SomeTransformer</code> has a bug? That’s where the Debugger comes in.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="1-Add-Dependencies"><a href="#1-Add-Dependencies" class="headerlink" title="1. Add Dependencies"></a>1. Add Dependencies</h3><p>First, import the <code>camel-test</code> package:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.camel<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>camel-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.16.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2-Create-a-Debugger-Class"><a href="#2-Create-a-Debugger-Class" class="headerlink" title="2. Create a Debugger Class"></a>2. Create a Debugger Class</h3><p>Extend <code>CamelTestSupport</code>, a convenient test base class provided by Camel:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CamelDebugger</span> <span class="keyword">extends</span> <span class="title class_">CamelTestSupport</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> RoutesBuilder <span class="title function_">createRouteBuilder</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RouteBuilder</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">configure</span><span class="params">()</span> &#123;</span><br><span class="line">                from(<span class="string">&quot;direct:start&quot;</span>)</span><br><span class="line">                    .log(<span class="string">&quot;Received: $&#123;body&#125;&quot;</span>)</span><br><span class="line">                    .bean(SomeTransformer.class)</span><br><span class="line">                    .to(<span class="string">&quot;mock:result&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">debugBefore</span><span class="params">(Exchange exchange, Processor processor, </span></span><br><span class="line"><span class="params">                                 ProcessorDefinition&lt;?&gt; definition, String id, String label)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;Before &#123;&#125;: body=&#123;&#125;, headers=&#123;&#125;&quot;</span>, </span><br><span class="line">            id, exchange.getIn().getBody(), exchange.getIn().getHeaders());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">debugAfter</span><span class="params">(Exchange exchange, Processor processor, </span></span><br><span class="line"><span class="params">                                ProcessorDefinition&lt;?&gt; definition, String id, String label, <span class="type">long</span> timeTaken)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;After &#123;&#125;: body=&#123;&#125;, took &#123;&#125;ms&quot;</span>, </span><br><span class="line">            id, exchange.getIn().getBody(), timeTaken);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Key-Methods-Explained"><a href="#Key-Methods-Explained" class="headerlink" title="Key Methods Explained"></a>Key Methods Explained</h2><p>Here are the key debug-related methods in <code>CamelTestSupport</code>:</p><h3 id="createCamelContext"><a href="#createCamelContext" class="headerlink" title="createCamelContext()"></a><code>createCamelContext()</code></h3><p>Customize the CamelContext — you can configure components, register beans, etc. This is suitable for scenarios where you need precise control over the test environment.</p><h3 id="createRouteBuilder"><a href="#createRouteBuilder" class="headerlink" title="createRouteBuilder()"></a><code>createRouteBuilder()</code></h3><p>Use the default CamelContext and only add your own route. This is the simplest approach and works well for most debugging scenarios.</p><h3 id="debugBefore-and-debugAfter"><a href="#debugBefore-and-debugAfter" class="headerlink" title="debugBefore() and debugAfter()"></a><code>debugBefore()</code> and <code>debugAfter()</code></h3><p>These two methods are the core of the Debugger:</p><ul><li><p><strong><code>debugBefore</code></strong>: Called <strong>before</strong> a message is processed by each processor. Parameters include the current Exchange (with message body, headers, and properties), the Processor about to be executed, the route node ID, and label. Use this to <strong>confirm “what does the message look like when entering this node”</strong>.</p></li><li><p><strong><code>debugAfter</code></strong>: Called <strong>after</strong> the message is processed. Includes an additional <code>timeTaken</code> parameter indicating how long this processor took to execute. Use this to <strong>compare the before-and-after changes</strong> and to <strong>identify performance bottlenecks</strong>.</p></li></ul><h2 id="Practical-Example-Troubleshooting-Message-Loss"><a href="#Practical-Example-Troubleshooting-Message-Loss" class="headerlink" title="Practical Example: Troubleshooting Message Loss"></a>Practical Example: Troubleshooting Message Loss</h2><p>Suppose a message disappears after a certain node in your route. Here’s how you would debug it:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">debugAfter</span><span class="params">(Exchange exchange, Processor processor,</span></span><br><span class="line"><span class="params">                            ProcessorDefinition&lt;?&gt; definition, String id, String label, <span class="type">long</span> timeTaken)</span> &#123;</span><br><span class="line">    <span class="type">Object</span> <span class="variable">body</span> <span class="operator">=</span> exchange.getIn().getBody();</span><br><span class="line">    <span class="keyword">if</span> (body == <span class="literal">null</span>) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;⚠️ Message body is null after processor: &#123;&#125; (id=&#123;&#125;)&quot;</span>, label, id);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Log a warning for any processor taking more than 1 second</span></span><br><span class="line">    <span class="keyword">if</span> (timeTaken &gt; <span class="number">1000</span>) &#123;</span><br><span class="line">        log.warn(<span class="string">&quot;🐢 Slow processor: &#123;&#125; took &#123;&#125;ms&quot;</span>, label, timeTaken);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Alternative-Solutions"><a href="#Alternative-Solutions" class="headerlink" title="Alternative Solutions"></a>Alternative Solutions</h2><p>The Debugger is not the only option — Camel also provides other troubleshooting approaches:</p><table><thead><tr><th>Method</th><th>Use Case</th><th>Pros &amp; Cons</th></tr></thead><tbody><tr><td><strong>Debugger</strong></td><td>Local development debugging</td><td>Most comprehensive information, but requires starting a test</td></tr><tr><td><strong>Tracer Interceptor</strong></td><td>Production troubleshooting</td><td>Can be enabled globally, but impacts performance</td></tr><tr><td><strong>Log EIP (<code>log:</code> component)</strong></td><td>Inserting temporary logs in routes</td><td>Simplest, but requires modifying route code</td></tr><tr><td><strong>Hawtio Management Console</strong></td><td>Visual monitoring</td><td>Shows runtime status, but coarser debugging granularity</td></tr></tbody></table><h2 id="Complete-Code"><a href="#Complete-Code" class="headerlink" title="Complete Code"></a>Complete Code</h2><p>A complete runnable example is available here:<br><a href="https://github.com/lcy362/CamelDemo/blob/7aef2cc7661236499896022f6976c160b73b68e7/src/main/java/com/mallow/demo/camel/debugger/CamelDebugger.java">https://github.com/lcy362/CamelDemo</a></p><blockquote><p>This is part of the Camel series. If you’re using Camel for data routing or system integration, stay tuned for follow-up articles, or <a href="https://github.com/lcy362/CamelDemo">chat on GitHub</a>.</p></blockquote><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-When-should-I-use-Debugger-instead-of-Tracer"><a href="#Q-When-should-I-use-Debugger-instead-of-Tracer" class="headerlink" title="Q: When should I use Debugger instead of Tracer?"></a>Q: When should I use Debugger instead of Tracer?</h3><p>The Debugger is ideal for <strong>local development debugging</strong> — it provides the most comprehensive information, allowing you to set breakpoints node by node, inspect message bodies, and compare before-and-after changes. Tracer is better suited for <strong>production troubleshooting</strong> — when enabled globally, it can track the full route path of every message, though with some performance impact. A simple rule of thumb: use Debugger in dev for precise troubleshooting, and Tracer in prod for a quick overview of message flows.</p><h3 id="Q-What-alternative-debugging-tools-does-Camel-offer"><a href="#Q-What-alternative-debugging-tools-does-Camel-offer" class="headerlink" title="Q: What alternative debugging tools does Camel offer?"></a>Q: What alternative debugging tools does Camel offer?</h3><p>Beyond the Debugger, Camel provides the Tracer Interceptor (global message tracking), Log EIP (insert temporary logs into routes), and the Hawtio Management Console (visual monitoring). You can also temporarily add <code>log:</code> or <code>wireTap:</code> components into routes for on-the-spot troubleshooting.</p><h3 id="Q-Does-Debugger-impact-performance"><a href="#Q-Does-Debugger-impact-performance" class="headerlink" title="Q: Does Debugger impact performance?"></a>Q: Does Debugger impact performance?</h3><p>It does. The Debugger triggers callbacks before and after every processor execution. If your callbacks include heavy logging or serialization operations, route throughput will drop noticeably. This is why the Debugger is designed exclusively for local development and testing — it should never be enabled in production.</p><h3 id="Q-Is-Debugger-only-for-test-environments"><a href="#Q-Is-Debugger-only-for-test-environments" class="headerlink" title="Q: Is Debugger only for test environments?"></a>Q: Is Debugger only for test environments?</h3><p>Strictly speaking, yes. Camel’s Debugger is built on the <code>CamelTestSupport</code> base class and is designed as a testing tool. If you need to debug a running route, use alternatives like Tracer, the Hawtio console, or JMX monitoring.</p><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><ol><li><strong>Add dependency</strong>: Add the <code>camel-test</code> dependency to <code>pom.xml</code>, matching the Camel version used in your project.</li><li><strong>Create Debugger class</strong>: Extend <code>CamelTestSupport</code>, override <code>createRouteBuilder()</code> to define the route under test, and override <code>debugBefore()</code> &#x2F; <code>debugAfter()</code> to log message content and processing time.</li><li><strong>Configure debug route</strong>: In <code>createRouteBuilder</code>, define the complete route chain using <code>from().to()</code> or <code>from().bean().to()</code>.</li><li><strong>Run test</strong>: Run the Debugger class directly and observe the console output — use <code>debugBefore</code> to confirm message entry, and <code>debugAfter</code> to compare changes and identify slow nodes.</li></ol>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/1120/</id>
    <link href="https://lichuanyang.top/en/posts/1120/"/>
    <published>2017-08-03T11:56:00.000Z</published>
    <summary>Apache Camel debugger tool usage guide, helping to efficiently troubleshoot issues in Camel's highly encapsulated data routing framework.</summary>
    <title>Camel Series: Using Camel Debugger</title>
    <updated>2026-06-27T02:40:56.429Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="JStorm Source Code Analysis" scheme="https://lichuanyang.top/en/categories/JStorm-Source-Code-Analysis/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/en/tags/jstorm/"/>
    <category term="source-code-reading" scheme="https://lichuanyang.top/en/tags/source-code-reading/"/>
    <content>
      <![CDATA[<h2 id="Problem"><a href="#Problem" class="headerlink" title="Problem"></a>Problem</h2><p>Anyone who has used Storm or JStorm knows that if an uncaught exception occurs in bolt code, the worker process will exit. This article analyzes the specific design from a source code perspective. It’s actually not as simple as “an exception occurs and then the process crashes.”</p><h2 id="The-Truth"><a href="#The-Truth" class="headerlink" title="The Truth"></a>The Truth</h2><p>Let’s first look at the source code of BasicBoltExecutor:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple input)</span> &#123;</span><br><span class="line">    _collector.setContext(input);</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        _bolt.execute(input, _collector);</span><br><span class="line">        _collector.getOutputter().ack(input);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (FailedException e) &#123;</span><br><span class="line">        <span class="keyword">if</span> (e <span class="keyword">instanceof</span> ReportedFailedException) &#123;</span><br><span class="line">            _collector.reportError(e);</span><br><span class="line">        &#125;</span><br><span class="line">        _collector.getOutputter().fail(input);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>_bolt.execute(input, _collector)</code> executes the <code>execute</code> method in our custom bolt. As you can see, only Storm’s own <code>FailedException</code> is caught here, and a fail message is sent to mark the tuple as failed. All other exceptions are allowed to propagate.</p><p>The outer layer is the <code>processTupleEvent</code> method of BoltExecutors:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!isSystemBolt &amp;&amp; tuple.getSourceStreamId().equals(Common.TOPOLOGY_MASTER_CONTROL_STREAM_ID)) &#123;</span><br><span class="line">        backpressureTrigger.handle(tuple);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        bolt.execute(tuple);</span><br><span class="line">    &#125;</span><br><span class="line">&#125; <span class="keyword">catch</span> (Throwable e) &#123;</span><br><span class="line">    error = e;</span><br><span class="line">    LOG.error(<span class="string">&quot;bolt execute error &quot;</span>, e);</span><br><span class="line">    report_error.report(e);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Here, all exceptions are caught, but only <code>report_error</code> is called — no fail message is sent. The related tuple can only be marked as failed after timing out.</p><p>Now let’s look at the specific implementation of <code>report_error.report(e)</code>. From the constructor, we can see that <code>report_error</code> is a <code>TaskReportErrorAndDie</code> class:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">report</span><span class="params">(Throwable error)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.reporterror.report(error);</span><br><span class="line">    <span class="built_in">this</span>.haltfn.run();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Here, <code>reporterror</code> is an <code>AsyncLoopDefaultKill</code> class:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">    JStormUtils.halt_process(<span class="number">1</span>, <span class="string">&quot;Async loop died!&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This is the final step of the entire process. The <code>JStormUtils.halt_process()</code> method prints an “Async loop died!” log message and then kills the worker process.</p><h2 id="Reflection"><a href="#Reflection" class="headerlink" title="Reflection"></a>Reflection</h2><p>From the code, we can see that for JStorm, “worker exits after exception” is an intentionally designed feature, not a sign of program fragility. I speculate that the design philosophy here is: for known exceptions, developers catch them and re-throw <code>FailedException</code> to mark messages as failed; for unknown exceptions, the process is forced to exit directly, avoiding excessive catching that would mask problems.</p><p>However, while that may be the reasoning, I still have reservations about this design. After all, Storm differs from regular Java programs — Storm worker processes are automatically restarted after exiting, so this exception handling approach cannot achieve a fail-fast effect.</p><p>On the contrary, the continuous restarting of workers can bring about other problems. Furthermore, instead of actively marking messages as failed, waiting for a timeout — if the timeout is set too long (though an excessively long timeout is unreasonable in itself) — can also introduce issues. For example, with KafkaSpout, no new data will be fetched until a message has been acked. If one message has to wait for timeout, all data in the same partition cannot be processed during that timeout period.</p><p>From another perspective, if all exceptions were handled like <code>FailedException</code>, since data failures are visible after the exception, it wouldn’t mask the problem either.</p><p>Therefore, I believe this handling logic still requires careful consideration.</p><p>Source: <a href="https://lichuanyang.top/en/posts/15594/">https://lichuanyang.top/en/posts/15594/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/15594/</id>
    <link href="https://lichuanyang.top/en/posts/15594/"/>
    <published>2017-08-03T11:29:00.000Z</published>
    <summary>An analysis from source code perspective of the mechanism where uncaught exceptions in JStorm bolts cause worker process termination, revealing the complete design behind it.</summary>
    <title>JStorm Source Code Analysis: Bolt Exception Handling</title>
    <updated>2026-06-27T02:42:01.670Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="logging" scheme="https://lichuanyang.top/en/tags/logging/"/>
    <content>
      <![CDATA[<p>Most Java developers manage log output through configuration files (<code>log4j.properties</code> or <code>log4j.xml</code>). But in certain operational scenarios, dynamically modifying logging configuration at runtime offers greater flexibility — for example, temporarily enabling DEBUG level during online troubleshooting, or dynamically pushing logs to Kafka for centralized analysis.</p><p>Log4j 1.x’s API fully supports runtime configuration changes via code. This article uses KafkaAppender as an example to demonstrate how to dynamically add an appender.</p><span id="more"></span><h2 id="Static-Configuration-vs-Dynamic-Configuration"><a href="#Static-Configuration-vs-Dynamic-Configuration" class="headerlink" title="Static Configuration vs. Dynamic Configuration"></a>Static Configuration vs. Dynamic Configuration</h2><table><thead><tr><th>Approach</th><th>Use Case</th><th>Pros and Cons</th></tr></thead><tbody><tr><td><strong>Config file</strong> (static)</td><td>Daily development, routine deployment</td><td>Simple, standardized; but requires restart to apply changes</td></tr><tr><td><strong>API programming</strong> (dynamic)</td><td>Online troubleshooting, operational automation</td><td>Flexible, no restart needed; but requires API familiarity</td></tr></tbody></table><h2 id="Basic-Example-Dynamically-Adding-a-KafkaAppender"><a href="#Basic-Example-Dynamically-Adding-a-KafkaAppender" class="headerlink" title="Basic Example: Dynamically Adding a KafkaAppender"></a>Basic Example: Dynamically Adding a KafkaAppender</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.apache.log4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.apache.log4j.PatternLayout;</span><br><span class="line"><span class="keyword">import</span> org.apache.log4j.Level;</span><br><span class="line"><span class="keyword">import</span> org.apache.log4j.net.SocketAppender; <span class="comment">// or others</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// An example utility method</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Log4jConfigurator</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">addKafkaAppender</span><span class="params">(String loggerName, String broker, </span></span><br><span class="line"><span class="params">                                          String topic, String layout)</span> &#123;</span><br><span class="line">        <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> Logger.getLogger(loggerName);</span><br><span class="line">        </span><br><span class="line">        <span class="type">KafkaLog4jAppender</span> <span class="variable">kafkaAppender</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">KafkaLog4jAppender</span>();</span><br><span class="line">        kafkaAppender.setBrokerList(broker);</span><br><span class="line">        kafkaAppender.setTopic(topic);</span><br><span class="line">        kafkaAppender.setCompressionType(<span class="string">&quot;gzip&quot;</span>);</span><br><span class="line">        kafkaAppender.setSyncSend(<span class="literal">false</span>);</span><br><span class="line">        kafkaAppender.setLayout(<span class="keyword">new</span> <span class="title class_">PatternLayout</span>(layout));</span><br><span class="line">        kafkaAppender.activateOptions();</span><br><span class="line">        </span><br><span class="line">        logger.addAppender(kafkaAppender);</span><br><span class="line">        logger.setLevel(Level.INFO);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Key steps</strong>:</p><ol><li><strong>Create an Appender instance</strong>, setting parameters like target address and format</li><li><strong>Call <code>activateOptions()</code></strong> — activates the configuration; many Appenders initialize connections at this step</li><li><strong><code>logger.addAppender()</code></strong> adds it to the target Logger</li><li><strong>Set the log level</strong> — only logs at or above this level will be processed by the Appender</li></ol><h2 id="Three-Real-World-Scenarios"><a href="#Three-Real-World-Scenarios" class="headerlink" title="Three Real-World Scenarios"></a>Three Real-World Scenarios</h2><h3 id="Scenario-1-Temporarily-Enable-DEBUG-Online"><a href="#Scenario-1-Temporarily-Enable-DEBUG-Online" class="headerlink" title="Scenario 1: Temporarily Enable DEBUG Online"></a>Scenario 1: Temporarily Enable DEBUG Online</h3><p>When an issue occurs in production, you need more detailed logs, but globally switching from INFO to DEBUG would generate a flood of data. Instead, dynamically adjust only a specific Logger:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Temporarily enable DEBUG only for the com.example.payment package</span></span><br><span class="line"><span class="type">Logger</span> <span class="variable">paymentLogger</span> <span class="operator">=</span> Logger.getLogger(<span class="string">&quot;com.example.payment&quot;</span>);</span><br><span class="line">paymentLogger.setLevel(Level.DEBUG);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Restore after troubleshooting</span></span><br><span class="line">paymentLogger.setLevel(Level.INFO);</span><br></pre></td></tr></table></figure><p>Expose this capability through an HTTP endpoint, and you can enable DEBUG with a single click in the browser.</p><h3 id="Scenario-2-Dynamically-Route-Logs-to-Kafka"><a href="#Scenario-2-Dynamically-Route-Logs-to-Kafka" class="headerlink" title="Scenario 2: Dynamically Route Logs to Kafka"></a>Scenario 2: Dynamically Route Logs to Kafka</h3><p>This is the main example above. It’s ideal for real-time log analysis — pushing application logs through Kafka to platforms like ELK or Flink.</p><h3 id="Scenario-3-Dynamically-Switch-Log-Files"><a href="#Scenario-3-Dynamically-Switch-Log-Files" class="headerlink" title="Scenario 3: Dynamically Switch Log Files"></a>Scenario 3: Dynamically Switch Log Files</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Switch from the current file to a new rolling file</span></span><br><span class="line"><span class="type">Logger</span> <span class="variable">rootLogger</span> <span class="operator">=</span> Logger.getRootLogger();</span><br><span class="line">rootLogger.removeAllAppenders();</span><br><span class="line"></span><br><span class="line"><span class="type">DailyRollingFileAppender</span> <span class="variable">newAppender</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DailyRollingFileAppender</span>();</span><br><span class="line">newAppender.setFile(<span class="string">&quot;/var/log/myapp/app.log&quot;</span>);</span><br><span class="line">newAppender.setDatePattern(<span class="string">&quot;&#x27;.&#x27;yyyy-MM-dd&quot;</span>);</span><br><span class="line">newAppender.setLayout(<span class="keyword">new</span> <span class="title class_">PatternLayout</span>(<span class="string">&quot;%d [%t] %-5p %c - %m%n&quot;</span>));</span><br><span class="line">newAppender.activateOptions();</span><br><span class="line"></span><br><span class="line">rootLogger.addAppender(newAppender);</span><br></pre></td></tr></table></figure><h2 id="Important-Considerations"><a href="#Important-Considerations" class="headerlink" title="Important Considerations"></a>Important Considerations</h2><ul><li><strong>Thread safety</strong>: Log4j 1.x is not fully thread-safe. When dynamically modifying configuration in a multi-threaded environment, use synchronization locks or manage via JMX.</li><li><strong>Memory leaks</strong>: Remember to <code>removeAppender()</code> dynamically added Appenders when they are no longer needed, especially those with network connections (Kafka&#x2F;Socket).</li><li><strong>Log4j 2 is friendlier</strong>: Log4j 2 provides a native <code>Configurator</code> API that is thread-safe and supports async Loggers, making it a better choice for new projects.</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Log4j 2&#x27;s dynamic configuration (more modern)</span></span><br><span class="line">Configurator.setLevel(<span class="string">&quot;com.example&quot;</span>, Level.DEBUG);</span><br></pre></td></tr></table></figure><p>If you’re still on Log4j 1.x, the dynamic API above can handle many operational scenarios. For new projects, consider going straight to Log4j 2 or SLF4J + Logback.</p><hr><h2 id="Quick-Start-Guide"><a href="#Quick-Start-Guide" class="headerlink" title="Quick Start Guide"></a>Quick Start Guide</h2><h3 id="Step-1-Identify-the-Requirement"><a href="#Step-1-Identify-the-Requirement" class="headerlink" title="Step 1: Identify the Requirement"></a>Step 1: Identify the Requirement</h3><p>Clarify the scenario for dynamically adding an Appender: temporarily enabling DEBUG logs, pushing logs to Kafka, or dynamically switching log files. Identify the target Logger name and the required Appender type.</p><h3 id="Step-2-Create-Appender-Instance"><a href="#Step-2-Create-Appender-Instance" class="headerlink" title="Step 2: Create Appender Instance"></a>Step 2: Create Appender Instance</h3><p>Create the corresponding Appender instance based on your requirements, such as <code>KafkaLog4jAppender</code>, <code>DailyRollingFileAppender</code>, etc. Use <code>Logger.getLogger(name)</code> to obtain the target Logger object.</p><h3 id="Step-3-Configure-Parameters"><a href="#Step-3-Configure-Parameters" class="headerlink" title="Step 3: Configure Parameters"></a>Step 3: Configure Parameters</h3><p>Set the necessary Appender parameters, such as Kafka broker address and topic, file path and rolling strategy, log format <code>PatternLayout</code>, etc.</p><h3 id="Step-4-Activate-and-Add-to-Logger"><a href="#Step-4-Activate-and-Add-to-Logger" class="headerlink" title="Step 4: Activate and Add to Logger"></a>Step 4: Activate and Add to Logger</h3><p>Call <code>appender.activateOptions()</code> to activate the configuration (initialization operations such as establishing connections), then add the Appender to the target Logger via <code>logger.addAppender(appender)</code>. Adjust the log level as needed.</p><h3 id="Step-5-Verify-Log-Output"><a href="#Step-5-Verify-Log-Output" class="headerlink" title="Step 5: Verify Log Output"></a>Step 5: Verify Log Output</h3><p>Trigger business logic to generate logs and confirm that logs are output as expected to the target destination (Kafka consumers receive messages, new log entries appear in files, etc.). Remember to call <code>removeAppender</code> after troubleshooting to release resources.</p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/42764/</id>
    <link href="https://lichuanyang.top/en/posts/42764/"/>
    <published>2017-06-30T11:30:00.000Z</published>
    <summary>A practical example of dynamically modifying Log4j configuration through code, demonstrating runtime appender addition using KafkaAppender as an example.</summary>
    <title>Dynamically Adding Appenders to Log4j</title>
    <updated>2026-06-27T03:49:52.427Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="testing" scheme="https://lichuanyang.top/en/tags/testing/"/>
    <content>
      <![CDATA[<h2 id="Why-Benchmark-Testing-is-Needed"><a href="#Why-Benchmark-Testing-is-Needed" class="headerlink" title="Why Benchmark Testing is Needed"></a>Why Benchmark Testing is Needed</h2><p>Sometimes we need to write some simple performance test code. I happened to come across an experience-based post on Stack Overflow: <a href="https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java">https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java</a> — how to write benchmarks to minimize environmental influence.</p><p>Here’s my translation:</p><h2 id="Writing-Reliable-Benchmarks"><a href="#Writing-Reliable-Benchmarks" class="headerlink" title="Writing Reliable Benchmarks"></a>Writing Reliable Benchmarks</h2><p>Tips from a Java HotSpot author on writing microbenchmarks:</p><p>Rule 0: Read good papers on the JVM and microbenchmarks. For example, <a href="https://www.ibm.com/developerworks/java/library/j-jtp02225/">https://www.ibm.com/developerworks/java/library/j-jtp02225/</a>. Don’t have too high expectations for these kinds of tests; they can only provide limited insight into JVM performance.</p><p>Rule 1: Always include a warm-up phase that runs until all initialization and compilation has been triggered. (The number of iterations during warm-up can be reduced; the rule of thumb is tens of thousands of loops.)</p><p>Rule 2: Always run with parameters like <code>-XX:+PrintCompilation -verbose:gc</code> so you can determine whether compilation phases and other parts of the JVM are doing unexpected work during timing.</p><p>Rule 2.1: Print messages at the beginning and end of the timing and warm-up phases, so you can determine whether there’s Rule 2 output during timing.</p><p>Rule 3: Understand the difference between -client and -server, and between OSR and regular compilation. -server is better than -client, and regular compilation is better than OSR.</p><h2 id="Common-Pitfalls"><a href="#Common-Pitfalls" class="headerlink" title="Common Pitfalls"></a>Common Pitfalls</h2><p>Rule 4: Be aware of initialization effects. Don’t print results from the first timing run, unless you’re testing class loading. Rule 2 is your first line of defense against this effect.</p><p>Rule 5: Be aware of compiler optimization and recompilation effects. Don’t use any code paths during timing, as the compiler may optimize based on some optimistic assumptions, leading to that path never being used, which allows the compiler to garbage-collect and recompile the code. Rule 2 is your first line of defense against this effect.</p><p>Rule 6: Use appropriate tools to observe the compiler’s work, and be prepared for it to produce some surprising code. Check the code yourself before forming theories about what makes things faster or slower.</p><p>Rule 7: Reduce noise in measurements. Run benchmarks on a quiet machine, run them several times, and discard outliers. Use <code>-Xbatch</code> to serialize the compiler with the application, and consider setting <code>-XX:CICompilerCount=1</code> to prevent the compiler from running parallel to itself.</p><h2 id="Getting-Started-with-JMH"><a href="#Getting-Started-with-JMH" class="headerlink" title="Getting Started with JMH"></a>Getting Started with JMH</h2><p>Rule 8: Use some libraries designed for benchmarking, as they may be more efficient. For example JMH, Caliper, UCSD Benchmarks for Java, etc.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/4987/">https://lichuanyang.top/en/posts/4987/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/4987/</id>
    <link href="https://lichuanyang.top/en/posts/4987/"/>
    <published>2017-06-05T12:39:00.000Z</published>
    <summary>Translated article: Java benchmark writing experiences, introducing JMH usage and common benchmark pitfalls.</summary>
    <title>[Translation] Some Experiences Writing Benchmarks in Java</title>
    <updated>2026-06-27T01:10:12.398Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="maven" scheme="https://lichuanyang.top/en/tags/maven/"/>
    <content>
      <![CDATA[<p>Mirror and repository in Maven are two easily confused concepts, as both are used to configure the addresses of remote Maven repositories. As the name suggests, a repository directly configures the site address, while a mirror acts as a site’s mirror, proxying requests to one or several sites, achieving a complete replacement of repositories.</p><h2 id="Repository"><a href="#Repository" class="headerlink" title="Repository"></a>Repository</h2><p>There are two ways to configure multiple repositories: configuring multiple profiles or configuring multiple repositories within a single profile. When configuring multiple profiles, you also need to configure activeProfiles for the configurations to take effect.</p><p>Configuration example:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line">      &lt;profiles&gt;</span><br><span class="line">&lt;/profile&gt;</span><br><span class="line">&lt;profile&gt;</span><br><span class="line">&lt;id&gt;central&lt;/id&gt;</span><br><span class="line">&lt;repositories&gt;</span><br><span class="line">&lt;repository&gt;</span><br><span class="line">&lt;id&gt;central&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://search.maven.org/&lt;/url&gt;</span><br><span class="line">&lt;!-- &lt;releases&gt; &lt;enabled&gt;true&lt;/enabled&gt; &lt;/releases&gt; &lt;snapshots&gt; &lt;enabled&gt;true&lt;/enabled&gt; </span><br><span class="line">&lt;/snapshots&gt; --&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;false&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/repository&gt;</span><br><span class="line">&lt;/repositories&gt;</span><br><span class="line">&lt;pluginRepositories&gt;</span><br><span class="line">&lt;pluginRepository&gt;</span><br><span class="line">&lt;id&gt;central&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://search.maven.org/&lt;/url&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;false&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/pluginRepository&gt;</span><br><span class="line">&lt;/pluginRepositories&gt;</span><br><span class="line">&lt;/profile&gt;</span><br><span class="line">&lt;profile&gt;</span><br><span class="line">&lt;id&gt;aliyun&lt;/id&gt;</span><br><span class="line">&lt;repositories&gt;</span><br><span class="line">&lt;repository&gt;</span><br><span class="line">&lt;id&gt;aliyun&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://maven.aliyun.com/nexus/content/groups/public&lt;/url&gt;</span><br><span class="line">&lt;!-- &lt;releases&gt; &lt;enabled&gt;true&lt;/enabled&gt; &lt;/releases&gt; &lt;snapshots&gt; &lt;enabled&gt;true&lt;/enabled&gt; </span><br><span class="line">&lt;/snapshots&gt; --&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/repository&gt;</span><br><span class="line">&lt;/repositories&gt;</span><br><span class="line">&lt;pluginRepositories&gt;</span><br><span class="line">&lt;pluginRepository&gt;</span><br><span class="line">&lt;id&gt;aliyun&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://maven.aliyun.com/nexus/content/groups/public&lt;/url&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/pluginRepository&gt;</span><br><span class="line">&lt;/pluginRepositories&gt;</span><br><span class="line">&lt;/profile&gt;</span><br><span class="line">          &lt;/profiles&gt;</span><br><span class="line"></span><br><span class="line">          &lt;activeProfiles&gt;</span><br><span class="line">&lt;activeProfile&gt;aliyun&lt;/activeProfile&gt;</span><br><span class="line">&lt;activeProfile&gt;central&lt;/activeProfile&gt;</span><br><span class="line">  &lt;/activeProfiles&gt;</span><br></pre></td></tr></table></figure><p>Single profile, multiple repository configuration is also similar.</p><p>This way, multiple site configurations are achieved. When downloading dependencies, Maven will try to download from each address in the order configured from top to bottom, until a successful download is made.</p><h2 id="Mirror"><a href="#Mirror" class="headerlink" title="Mirror"></a>Mirror</h2><p>In my opinion, the existence of mirror is somewhat redundant. If you don’t want to use the address configured in the repository, you can simply change it directly, without adding another mirror configuration.</p><p>If both settings.xml and pom have configured repositories, the configured mirror can take effect on both configuration files, which may be the only meaning of mirror’s existence.</p><p>Mirror configuration example:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">mirror</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">id</span>&gt;</span>nexus-aliyun<span class="tag">&lt;/<span class="name">id</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">mirrorOf</span>&gt;</span>*<span class="tag">&lt;/<span class="name">mirrorOf</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">name</span>&gt;</span>Nexus aliyun<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">url</span>&gt;</span>http://maven.aliyun.com/nexus/content/groups/public<span class="tag">&lt;/<span class="name">url</span>&gt;</span></span><br><span class="line">       <span class="tag">&lt;/<span class="name">mirror</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Use mirrorOf to specify which repository this mirror targets. Setting it to * means it will proxy requests to all repositories.</p><p>Note that, unlike repositories, when multiple mirrors are configured for the same repository, they are in a backup relationship with each other. They will only switch to another one when the repository is unreachable. If the repository is reachable but a package cannot be found, it will not try the next address.</p><h2 id="Therefore-in-general-whether-configuring-domestic-Maven-repositories-or-configuring-private-repositories-like-Nexus-you-can-directly-configure-them-as-repositories-This-way-even-if-some-of-these-configured-repositories-have-issues-and-some-packages-cannot-be-downloaded-you-can-still-try-other-repositories"><a href="#Therefore-in-general-whether-configuring-domestic-Maven-repositories-or-configuring-private-repositories-like-Nexus-you-can-directly-configure-them-as-repositories-This-way-even-if-some-of-these-configured-repositories-have-issues-and-some-packages-cannot-be-downloaded-you-can-still-try-other-repositories" class="headerlink" title="Therefore, in general, whether configuring domestic Maven repositories or configuring private repositories like Nexus, you can directly configure them as repositories. This way, even if some of these configured repositories have issues and some packages cannot be downloaded, you can still try other repositories."></a>Therefore, in general, whether configuring domestic Maven repositories or configuring private repositories like Nexus, you can directly configure them as repositories. This way, even if some of these configured repositories have issues and some packages cannot be downloaded, you can still try other repositories.</h2><p>Source: <a href="https://lichuanyang.top/en/posts/32793/">https://lichuanyang.top/en/posts/32793/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/32793/</id>
    <link href="https://lichuanyang.top/en/posts/32793/"/>
    <published>2017-05-19T11:40:00.000Z</published>
    <summary>Clarify the differences between mirror and repository in Maven, and introduce the correct way to configure multiple repositories and mirror proxies.</summary>
    <title>Maven Mirror and Repository: Configuring Multiple Repositories</title>
    <updated>2026-06-27T03:50:07.524Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="maven" scheme="https://lichuanyang.top/en/tags/maven/"/>
    <content>
      <![CDATA[<h2 id="What-Is-a-Fat-Jar"><a href="#What-Is-a-Fat-Jar" class="headerlink" title="What Is a Fat Jar"></a>What Is a Fat Jar</h2><p>Eclipse has a feature called “Export Runnable JAR File” that packages a project along with all its dependencies into a Fat JAR, with a specified Main class, so you can run the code directly using <code>java jar xxx.jar</code>.</p><span id="more"></span><h2 id="Maven-Shade-Plugin-Configuration"><a href="#Maven-Shade-Plugin-Configuration" class="headerlink" title="Maven Shade Plugin Configuration"></a>Maven Shade Plugin Configuration</h2><p>However, what if you’re not using Eclipse? Actually, with the help of Maven, we can easily achieve the same functionality. Maven provides a Shade Plugin that can be used to build Fat JARs, and it also supports specifying the Main class.</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">project</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line">  <span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>maven-shade-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">executions</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">phase</span>&gt;</span>package<span class="tag">&lt;/<span class="name">phase</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">goals</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">goal</span>&gt;</span>shade<span class="tag">&lt;/<span class="name">goal</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">goals</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">transformers</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">transformer</span> <span class="attr">implementation</span>=<span class="string">&quot;org.apache.maven.plugins.shade.resource.ManifestResourceTransformer&quot;</span>&gt;</span></span><br><span class="line">                  <span class="tag">&lt;<span class="name">mainClass</span>&gt;</span>org.sonatype.haven.HavenCli<span class="tag">&lt;/<span class="name">mainClass</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">transformer</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;/<span class="name">transformers</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">executions</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line"><span class="tag">&lt;/<span class="name">project</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Common-Issues"><a href="#Common-Issues" class="headerlink" title="Common Issues"></a>Common Issues</h2><p>Then when you build with Maven, it will produce a directly runnable package.</p><p>Source: <a href="https://lichuanyang.top/en/posts/3945/">https://lichuanyang.top/en/posts/3945/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/3945/</id>
    <link href="https://lichuanyang.top/en/posts/3945/"/>
    <published>2017-05-14T01:32:00.000Z</published>
    <summary>Build executable Fat JARs with Maven Shade Plugin as an alternative to Eclipse's Export feature, specifying the Main class for one-click execution.</summary>
    <title>Building Executable JAR Packages with Maven Shade Plugin</title>
    <updated>2026-06-27T01:14:03.237Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <content>
      <![CDATA[<p>ActiveMQ provides a plugin development mechanism (<a href="http://activemq.apache.org/developing-plugins.html">http://activemq.apache.org/developing-plugins.html</a>) that allows you to conveniently add various custom functionalities. The effect is similar to directly modifying ActiveMQ’s source code, but using plugins is much better than modifying source code in terms of both convenience and risk. In theory, we can implement almost any functionality imaginable through this approach.</p><h2 id="Development-Guide"><a href="#Development-Guide" class="headerlink" title="Development Guide"></a>Development Guide</h2><p>First, you need to add the ActiveMQ dependency to your project. For convenience, you can use the <code>activemq-all</code> package directly.</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.activemq<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>activemq-all<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.13.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Developing an ActiveMQ plugin is very straightforward — you only need to implement two classes.</p><p>One is the plugin class, which needs to implement the <code>BrokerPlugin</code> interface, and then implement the <code>installPlugin</code> method.</p><p>The sole purpose of this method is to specify a broker class:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Broker <span class="title function_">installPlugin</span><span class="params">(Broker broker)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">LimitQueueSIzeBroker</span>(broker);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Next, you need to implement a broker class. In most cases, you can extend the <code>BrokerFilter</code> class, override the methods you need, and after the constructor and each overridden method finishes execution, call the corresponding method on the superclass. This way, it won’t affect ActiveMQ’s actual runtime.</p><p>For example:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">FoxBroker</span><span class="params">(Broker next)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>(next);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">send</span><span class="params">(ProducerBrokerExchange producerExchange, Message messageSend)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> producerExchange.getConnectionContext().getConnection().getRemoteAddress();</span><br><span class="line">        <span class="type">String</span> <span class="variable">destinationName</span> <span class="operator">=</span> messageSend.getDestination().getPhysicalName();</span><br><span class="line">        logger.info(<span class="string">&quot;send_&quot;</span> + destinationName + <span class="string">&quot; &quot;</span>  + ip);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        logger.error(<span class="string">&quot;activemq send log error: &quot;</span> + e, e);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">super</span>.send(producerExchange, messageSend);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Here we override ActiveMQ’s <code>send</code> method and add some logging.</p><p>Then package it and place the JAR file in ActiveMQ’s <code>lib</code> directory. Next, add the <code>plugins</code> module in <code>activemq.xml</code> and list the desired plugins inside:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://www.springframework.org/schema/beans&quot;</span> <span class="attr">id</span>=<span class="string">&quot;testPlugin&quot;</span> <span class="attr">class</span>=<span class="string">&quot;com.mallow.activemq.FoxBrokerPlugin&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://www.springframework.org/schema/beans&quot;</span> <span class="attr">id</span>=<span class="string">&quot;purgePlugin&quot;</span> <span class="attr">class</span>=<span class="string">&quot;com.mallow.activemq.LimitQueueSizePlugin&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Restart ActiveMQ, and you can see the effect when sending and receiving messages.</p><h2 id="Examples"><a href="#Examples" class="headerlink" title="Examples"></a>Examples</h2><p>You can refer to the code I have on GitHub: <a href="https://github.com/lcy362/FoxActivemqPlugin">https://github.com/lcy362/FoxActivemqPlugin</a></p><p>It provides two very simple plugin examples. <code>FoxBrokerPlugin</code> logs the IP addresses of producers and consumers when sending and receiving messages. <code>LimitQueueSizePlugin</code> can control the queue size — when the queue accumulates 1000 messages, new messages will be discarded, which is quite useful in test environments.</p><p>Additionally, ActiveMQ itself provides several commonly used plugins, including <code>LoggingBrokerPlugin</code>, <code>StatisticsBrokerPlugin</code>, etc., which you can also reference for implementation.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/61645/">https://lichuanyang.top/en/posts/61645/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/61645/</id>
    <link href="https://lichuanyang.top/en/posts/61645/"/>
    <published>2017-05-05T08:58:00.000Z</published>
    <summary>A complete guide to ActiveMQ plugin development, introducing how to conveniently add custom functionality through the plugin mechanism, which is safer and more efficient than directly modifying the source code.</summary>
    <title>ActiveMQ Plugin Development Guide and Examples</title>
    <updated>2026-06-27T02:41:53.772Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <content>
      <![CDATA[<p>When running a Java program directly from the command line, if you need to include third-party jar packages, the common approach is to use Maven&#x2F;Gradle to build a fat jar (bundling all dependencies). However, in lightweight scenarios — such as writing a small script, doing quick prototype validation, or temporarily running Java code in a CI environment — manually setting the classpath is often more straightforward.</p><span id="more"></span><h2 id="What-Is-Classpath"><a href="#What-Is-Classpath" class="headerlink" title="What Is Classpath?"></a>What Is Classpath?</h2><p>Classpath is the search path the JVM uses to locate class files. When you run <code>java com.example.Main</code>, the JVM searches each entry in the classpath for <code>com/example/Main.class</code>.</p><p>By default, the classpath only includes the current directory (<code>.</code>). If your program depends on other jar files, you need to add them to the classpath manually.</p><h2 id="Method-1-Shell-Script-Concatenation"><a href="#Method-1-Shell-Script-Concatenation" class="headerlink" title="Method 1: Shell Script Concatenation"></a>Method 1: Shell Script Concatenation</h2><p>Write a simple startup script that manually concatenates the paths of all dependency jars:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Define CLASSPATH</span></span><br><span class="line">CLASSPATH=your-main.jar:lib/dep1.jar:lib/dep2.jar:<span class="string">&quot;<span class="variable">$CLASSPATH</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Run</span></span><br><span class="line">java -classpath <span class="string">&quot;<span class="variable">$&#123;CLASSPATH&#125;</span>&quot;</span> com.example.Main</span><br></pre></td></tr></table></figure><p><strong>A few things to note</strong>:</p><ul><li><strong>Your own main class’s jar must also be included</strong> — this is easy to forget</li><li><strong>Linux&#x2F;macOS use <code>:</code> as the separator</strong>, Windows uses <code>;</code>:<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Linux</span></span><br><span class="line">java -classpath <span class="string">&quot;a.jar:b.jar&quot;</span> Main</span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows</span></span><br><span class="line">java -classpath <span class="string">&quot;a.jar;b.jar&quot;</span> Main</span><br></pre></td></tr></table></figure></li><li><strong>Quote paths that contain spaces</strong></li></ul><h2 id="Method-2-Wildcard-Batch-Inclusion"><a href="#Method-2-Wildcard-Batch-Inclusion" class="headerlink" title="Method 2: Wildcard Batch Inclusion"></a>Method 2: Wildcard Batch Inclusion</h2><p>If you have many jars in a <code>lib</code> directory, listing them one by one is tedious. Java 6+ supports wildcards:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -classpath <span class="string">&quot;your-main.jar:lib/*&quot;</span> com.example.Main</span><br></pre></td></tr></table></figure><p><code>lib/*</code> automatically matches all <code>.jar</code> files under <code>lib/</code>. <strong>Note</strong>: the wildcard only matches jars, not class files or subdirectories.</p><h2 id="Method-3-Auto-Generate-a-Startup-Script"><a href="#Method-3-Auto-Generate-a-Startup-Script" class="headerlink" title="Method 3: Auto-Generate a Startup Script"></a>Method 3: Auto-Generate a Startup Script</h2><p>If you use Maven, you can use the <code>maven-dependency-plugin</code> to auto-generate the classpath:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>maven-dependency-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">executions</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">goals</span>&gt;</span><span class="tag">&lt;<span class="name">goal</span>&gt;</span>build-classpath<span class="tag">&lt;/<span class="name">goal</span>&gt;</span><span class="tag">&lt;/<span class="name">goals</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">outputFile</span>&gt;</span>classpath.txt<span class="tag">&lt;/<span class="name">outputFile</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">executions</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Then read it in your script:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CLASSPATH=$(<span class="built_in">cat</span> target/classpath.txt):target/your-app.jar</span><br><span class="line">java -classpath <span class="string">&quot;<span class="variable">$&#123;CLASSPATH&#125;</span>&quot;</span> com.example.Main</span><br></pre></td></tr></table></figure><h2 id="When-to-Use-Manual-Classpath-vs-Fat-Jar"><a href="#When-to-Use-Manual-Classpath-vs-Fat-Jar" class="headerlink" title="When to Use Manual Classpath vs. Fat Jar?"></a>When to Use Manual Classpath vs. Fat Jar?</h2><table><thead><tr><th>Approach</th><th>Use Case</th><th>Pros</th><th>Cons</th></tr></thead><tbody><tr><td>Manual classpath</td><td>Quick prototyping, CI scripts, small tools</td><td>Simple, no extra build step</td><td>Easy to get paths wrong</td></tr><tr><td>Fat Jar (Shade&#x2F;Assembly)</td><td>Production deployment</td><td>Single file, runs anywhere</td><td>Slow build, large file</td></tr><tr><td>Docker image</td><td>Modern standard deployment</td><td>Best environment consistency</td><td>Requires Docker</td></tr></tbody></table><p>If you just want to quickly run some Java code on a server to validate an idea, manually putting together the classpath is the fastest way. But for production environments, use a fat jar or Docker image for a more standardized approach.</p><h2 id="Additionally-if-you-need-more-sophisticated-dependency-management-check-out-my-earlier-post-on-building-executable-jars-with-Maven-Shade-Plugin"><a href="#Additionally-if-you-need-more-sophisticated-dependency-management-check-out-my-earlier-post-on-building-executable-jars-with-Maven-Shade-Plugin" class="headerlink" title="Additionally, if you need more sophisticated dependency management, check out my earlier post on building executable jars with Maven Shade Plugin."></a>Additionally, if you need more sophisticated dependency management, check out my earlier post on <a href="/posts/3945/">building executable jars with Maven Shade Plugin</a>.</h2>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/65262/</id>
    <link href="https://lichuanyang.top/en/posts/65262/"/>
    <published>2017-05-03T10:42:00.000Z</published>
    <summary>Setting up CLASSPATH via a shell script to include third-party dependencies when running Java programs from the command line.</summary>
    <title>Running Java Programs from the Command Line by Adding Third-Party Jars to the Classpath</title>
    <updated>2026-06-27T03:51:55.449Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <content>
      <![CDATA[<h2 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h2><p>Recently I encountered an error when trying to run a fat jar:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">Exception in thread &quot;main&quot; java.lang.SecurityException: Invalid signature file digest for Manifest main attributes</span><br><span class="line">at sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:287)</span><br><span class="line">at sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:240)</span><br><span class="line">at java.util.jar.JarVerifier.processEntry(JarVerifier.java:317)</span><br><span class="line">at java.util.jar.JarVerifier.update(JarVerifier.java:228)</span><br><span class="line">at java.util.jar.JarFile.initializeVerifier(JarFile.java:348)</span><br><span class="line">at java.util.jar.JarFile.getInputStream(JarFile.java:415)</span><br><span class="line">at sun.misc.URLClassPath$JarLoader$2.getInputStream(URLClassPath.java:775)</span><br><span class="line">at sun.misc.Resource.cachedInputStream(Resource.java:77)</span><br><span class="line">at sun.misc.Resource.getByteBuffer(Resource.java:160)</span><br><span class="line">at java.net.URLClassLoader.defineClass(URLClassLoader.java:436)</span><br><span class="line">at java.net.URLClassLoader.access$100(URLClassLoader.java:71)</span><br><span class="line">at java.net.URLClassLoader$1.run(URLClassLoader.java:361)</span><br><span class="line">at java.net.URLClassLoader$1.run(URLClassLoader.java:355)</span><br><span class="line">at java.security.AccessController.doPrivileged(Native Method)</span><br><span class="line">at java.net.URLClassLoader.findClass(URLClassLoader.java:354)</span><br><span class="line">at java.lang.ClassLoader.loadClass(ClassLoader.java:425)</span><br><span class="line">at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)</span><br><span class="line">at java.lang.ClassLoader.loadClass(ClassLoader.java:358)</span><br><span class="line">at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)</span><br></pre></td></tr></table></figure><h2 id="Root-Cause-Signature-File-Conflict"><a href="#Root-Cause-Signature-File-Conflict" class="headerlink" title="Root Cause: Signature File Conflict"></a>Root Cause: Signature File Conflict</h2><p>After some investigation, I found it’s related to jar signing. For details on signing, you can refer to: <a href="http://www.cnblogs.com/jackofhearts/p/jar_signing.html">http://www.cnblogs.com/jackofhearts/p/jar_signing.html</a></p><p>Some jar packages contain a <code>.SF</code> file in <code>META-INF</code>, which includes the hashes of the class files and resource files in the original jar package, used for verifying file integrity and other validations.</p><h2 id="The-Problem-Caused-by-Maven-Shade"><a href="#The-Problem-Caused-by-Maven-Shade" class="headerlink" title="The Problem Caused by Maven Shade"></a>The Problem Caused by Maven Shade</h2><p>However, when building a fat jar, we combine many jar packages into one. This means the fat jar ends up containing signature files from each individual jar, but they obviously cannot be validated against the final fat jar.</p><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>The solution is to remove all signature files during packaging. If you’re using Maven, you can use the shade plugin:</p><h2 id="Maven-Configuration-Example"><a href="#Maven-Configuration-Example" class="headerlink" title="Maven Configuration Example"></a>Maven Configuration Example</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">&lt;plugin&gt;</span><br><span class="line">     &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">     &lt;artifactId&gt;maven-shade-plugin&lt;/artifactId&gt;</span><br><span class="line">     &lt;version&gt;1.7.1&lt;/version&gt;</span><br><span class="line">     &lt;executions&gt;</span><br><span class="line">         &lt;execution&gt;</span><br><span class="line">             &lt;phase&gt;package&lt;/phase&gt;</span><br><span class="line">             &lt;goals&gt;</span><br><span class="line">                 &lt;goal&gt;shade&lt;/goal&gt;</span><br><span class="line">             &lt;/goals&gt;</span><br><span class="line">             &lt;configuration&gt;</span><br><span class="line">                 &lt;filters&gt;</span><br><span class="line">                     &lt;filter&gt;</span><br><span class="line">                         &lt;artifact&gt;*:*&lt;/artifact&gt;</span><br><span class="line">                         &lt;excludes&gt;</span><br><span class="line">                             &lt;exclude&gt;META-INF/*.SF&lt;/exclude&gt;</span><br><span class="line">                             &lt;exclude&gt;META-INF/*.DSA&lt;/exclude&gt;</span><br><span class="line">                             &lt;exclude&gt;META-INF/*.RSA&lt;/exclude&gt;</span><br><span class="line">                         &lt;/excludes&gt;</span><br><span class="line">                     &lt;/filter&gt;</span><br><span class="line">                 &lt;/filters&gt;</span><br><span class="line">             &lt;/configuration&gt;</span><br><span class="line">         &lt;/execution&gt;</span><br><span class="line">     &lt;/executions&gt;</span><br><span class="line"> &lt;/plugin&gt;</span><br></pre></td></tr></table></figure><h2 id="Prevention-Measures"><a href="#Prevention-Measures" class="headerlink" title="Prevention Measures"></a>Prevention Measures</h2><p>Source: <a href="https://lichuanyang.top/en/posts/2478/">https://lichuanyang.top/en/posts/2478/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/2478/</id>
    <link href="https://lichuanyang.top/en/posts/2478/"/>
    <published>2017-04-27T06:16:00.000Z</published>
    <summary>Solving the SecurityException caused by signature verification when creating fat jars, understanding the META-INF signature mechanism, and methods to exclude signature files.</summary>
    <title>Fixing the SecurityException: Invalid Signature File Digest for Manifest Main Attributes in Fat Jars</title>
    <updated>2026-06-27T03:50:56.863Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="redis" scheme="https://lichuanyang.top/en/tags/redis/"/>
    <category term="redis-cluster" scheme="https://lichuanyang.top/en/tags/redis-cluster/"/>
    <content>
      <![CDATA[<p>Redis Cluster is an official clustering solution provided by Redis. It has been available as a stable release since version 3.0, is widely adopted, and has stood the test of time. Personally, I believe it can fully replace other clustering solutions like Codis and Twemproxy.</p><h2 id="Cluster-Principles"><a href="#Cluster-Principles" class="headerlink" title="Cluster Principles"></a>Cluster Principles</h2><p>Cluster implements data sharding. A Redis Cluster contains 16,384 hash slots. Any key can be mapped to a specific slot using the formula <code>CRC16(key) % 16384</code>. Which slot belongs to which node is predetermined. Therefore, for any operation, the system first computes the target node based on the key, then performs the operation on that node.</p><p>In essence, compared to a single-node setup, cluster only adds the step of computing the sharding value for a key — it doesn’t add much complexity at all, so you can use it with confidence.</p><p>With this design, operations on nodes are also convenient and don’t impact the client:</p><ol><li><strong>Adding a node</strong>: Transfer some slots from existing nodes to the new node.</li><li><strong>Removing a node</strong>: Transfer the node’s slots to other nodes, then remove the empty node.</li><li><strong>Node failure</strong>: Operations routed to the failed node will fail, but the rest of the cluster remains accessible normally. This avoids the “avalanche” problem seen in consistent hashing. Single-node failures can also be mitigated by configuring replicas — when a node fails, its replica can take over.</li><li><strong>Node restart</strong>: Simply restart normally — the node will automatically read its previous configuration and rejoin the cluster.</li></ol><h2 id="Installation-Process"><a href="#Installation-Process" class="headerlink" title="Installation Process"></a>Installation Process</h2><p>Let’s walk through an actual installation. Since this is for testing purposes, installing two or three nodes is sufficient. For production use, you need at least 3 master nodes, and it’s recommended to also have at least 3 replica nodes.</p><h3 id="Installing-Redis"><a href="#Installing-Redis" class="headerlink" title="Installing Redis"></a>Installing Redis</h3><p>Download the latest stable version from the Redis official website (<a href="https://redis.io/download">https://redis.io/download</a>).</p><p>Using version 3.2.8 as an example, run the following commands after downloading:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ tar xzf redis-3.2.8.tar.gz</span><br><span class="line">$ cd redis-3.2.8</span><br><span class="line">$ make</span><br></pre></td></tr></table></figure><p>Then just extract and run <code>make</code>.</p><h3 id="Redis-Configuration-and-Startup"><a href="#Redis-Configuration-and-Startup" class="headerlink" title="Redis Configuration and Startup"></a>Redis Configuration and Startup</h3><p>After <code>make</code>, several commands will appear in the Redis <code>src</code> directory, including <code>redis-server</code>, <code>redis-cli</code>, etc. You can copy them to <code>/usr/local/bin</code> to run from any directory, or operate directly from the <code>src</code> directory.</p><p>Before starting, you need to modify the configuration. There’s a <code>redis.conf</code> file in the Redis root directory — you can edit it directly or copy it to another path for modification.</p><p>The main thing is to enable cluster mode and configure the cluster config file:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cluster-enabled yes</span><br><span class="line">cluster-config-file nodes.conf</span><br></pre></td></tr></table></figure><p>Other settings like <code>port</code>, <code>bind</code>, <code>protect-mode</code>, etc., can be modified as needed, or left as default. If you want to run multiple instances on a single machine, each instance needs its own config file, and settings like <code>port</code> and <code>cluster-config-file</code> must differ between instances.</p><p>Then start each instance with its respective config file:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">redis-server redis-6379.conf &amp;</span><br><span class="line">redis-server redis-6380.conf &amp;</span><br></pre></td></tr></table></figure><h3 id="Enabling-Cluster"><a href="#Enabling-Cluster" class="headerlink" title="Enabling Cluster"></a>Enabling Cluster</h3><p>Once all required nodes are running, you can start configuring the cluster. The recommended approach is to use the official <code>redis-trib.rb</code> tool — see the official documentation for details. Here, let’s try manual setup to deepen our understanding of how the cluster works.</p><p>First, enter any Redis instance using <code>redis-cli</code>, then run the <code>meet</code> command to connect to each of the other nodes:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cluster meet xxx:6380</span><br></pre></td></tr></table></figure><p>Then run <code>cluster nodes</code> to see the nodes have been added to the cluster.</p><p>Next, you need to assign slots. You can use the <code>cluster setslot</code> command, but since there are many slots, this is tedious. An alternative is to modify the <code>nodes.conf</code> file directly.</p><p>The <code>nodes.conf</code> file contains nearly all cluster-related information. Normally, you don’t need to modify it manually — Redis generates it automatically. You can open it to take a look. Each node has one line in a format like this:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected</span><br></pre></td></tr></table></figure><p>Slot assignments are appended at the end:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected 0-8000</span><br></pre></td></tr></table></figure><p>Note that all 16,384 slots (0–16383) must be assigned — none can be left unassigned. Each node in the cluster has its own <code>nodes.conf</code> file, and the slot configurations must be consistent across all files.</p><p>After making changes, restart all nodes. Execute <code>cluster info</code> in <code>redis-cli</code> to see key information:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">cluster_state:ok</span><br><span class="line">cluster_slots_assigned:16384</span><br><span class="line">cluster_slots_ok:16384</span><br><span class="line">cluster_slots_pfail:0</span><br><span class="line">cluster_slots_fail:0</span><br><span class="line">cluster_known_nodes:2</span><br><span class="line">cluster_size:1</span><br></pre></td></tr></table></figure><p>All slots have been assigned, and the cluster state shows <code>ok</code>. This completes the cluster setup.</p><p>Source: <a href="https://lichuanyang.top/en/posts/9329/">https://lichuanyang.top/en/posts/9329/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/9329/</id>
    <link href="https://lichuanyang.top/en/posts/9329/"/>
    <published>2017-04-14T12:02:00.000Z</published>
    <summary>A hands-on guide to Redis Cluster architecture, covering data sharding, failover, and practical deployment tips for production environments.</summary>
    <title>Understanding Redis Cluster Through Practice</title>
    <updated>2026-06-27T02:42:08.981Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="hbase" scheme="https://lichuanyang.top/en/tags/hbase/"/>
    <content>
      <![CDATA[<p>HBase’s storage model is a simple key-value store (rowkey → column family → qualifier → value). Unlike Redis, it doesn’t natively support data structures like Set, List, or Hash. Yet in real-world applications, we often need to store collection-type data — user tag sets, product attribute sets, friend lists in social graphs, to name a few.</p><p>How can we elegantly implement a Set in HBase? This article introduces an efficient approach that leverages HBase’s column (qualifier) characteristics.</p><span id="more"></span><h2 id="Requirements-Definition"><a href="#Requirements-Definition" class="headerlink" title="Requirements Definition"></a>Requirements Definition</h2><p>A proper Set storage solution must satisfy:</p><ol><li><strong>Automatic deduplication of elements</strong> — fundamental Set semantics</li><li><strong>Atomic operations on individual elements</strong> — adding, deleting, or checking one element should not affect others</li><li><strong>Convenient querying</strong> — fast retrieval of the entire Set, and fast existence checks for individual elements</li></ol><h2 id="Approach-Comparison"><a href="#Approach-Comparison" class="headerlink" title="Approach Comparison"></a>Approach Comparison</h2><h3 id="Approach-A-Full-Serialization"><a href="#Approach-A-Full-Serialization" class="headerlink" title="Approach A: Full Serialization"></a>Approach A: Full Serialization</h3><p>Serialize the entire Set as a byte array and store it in a single HBase cell.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Write</span></span><br><span class="line">Set&lt;String&gt; set = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;(Arrays.asList(<span class="string">&quot;a&quot;</span>, <span class="string">&quot;b&quot;</span>, <span class="string">&quot;c&quot;</span>));</span><br><span class="line"><span class="type">byte</span>[] bytes = serialize(set);</span><br><span class="line"><span class="type">Put</span> <span class="variable">put</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Put</span>(Bytes.toBytes(<span class="string">&quot;row1&quot;</span>));</span><br><span class="line">put.addColumn(Bytes.toBytes(<span class="string">&quot;cf&quot;</span>), Bytes.toBytes(<span class="string">&quot;set&quot;</span>), bytes);</span><br><span class="line">table.put(put);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Read → Modify → Overwrite</span></span><br><span class="line"><span class="type">Get</span> <span class="variable">get</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Get</span>(Bytes.toBytes(<span class="string">&quot;row1&quot;</span>));</span><br><span class="line"><span class="type">Result</span> <span class="variable">result</span> <span class="operator">=</span> table.get(get);</span><br><span class="line">Set&lt;String&gt; set = deserialize(result.getValue(Bytes.toBytes(<span class="string">&quot;cf&quot;</span>), Bytes.toBytes(<span class="string">&quot;set&quot;</span>)));</span><br><span class="line">set.add(<span class="string">&quot;d&quot;</span>);  <span class="comment">// modify</span></span><br><span class="line"><span class="comment">// Then write the whole thing back...</span></span><br></pre></td></tr></table></figure><p><strong>Obvious drawbacks</strong>: Every add&#x2F;delete&#x2F;modify requires a “read-modify-write” cycle, which is not atomic. Data loss is inevitable under concurrent access.</p><h3 id="Approach-B-One-Row-per-Element"><a href="#Approach-B-One-Row-per-Element" class="headerlink" title="Approach B: One Row per Element"></a>Approach B: One Row per Element</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rowkey       cf:qualifier     value</span><br><span class="line">user_a       cf:element       a</span><br><span class="line">user_b       cf:element       b</span><br><span class="line">user_c       cf:element       c</span><br></pre></td></tr></table></figure><p>Each Set element is stored as a separate row, with rowkey designed as <code>set_id + element</code>. Use a Scan with prefix matching to retrieve the entire Set.</p><p><strong>Drawback</strong>: Retrieving the entire Set requires scanning multiple rows — less efficient than Approach C.</p><h3 id="Approach-C-Qualifier-Approach-Recommended"><a href="#Approach-C-Qualifier-Approach-Recommended" class="headerlink" title="Approach C: Qualifier Approach (Recommended)"></a>Approach C: Qualifier Approach (Recommended)</h3><p>Store Set element values in the <strong>qualifier</strong>, with an arbitrary placeholder value (e.g., <code>&quot;1&quot;</code>):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rowkey       cf:qualifier     value</span><br><span class="line">set_123      cf:a             1</span><br><span class="line">set_123      cf:b             1</span><br><span class="line">set_123      cf:c             1</span><br></pre></td></tr></table></figure><p><strong>Core advantages</strong>:</p><table><thead><tr><th>Operation</th><th>HBase Command</th><th>Complexity</th></tr></thead><tbody><tr><td>Add element</td><td><code>put.addColumn(cf, &quot;d&quot;, &quot;1&quot;)</code></td><td>O(1)</td></tr><tr><td>Remove element</td><td><code>delete.addColumn(cf, &quot;d&quot;)</code></td><td>O(1)</td></tr><tr><td>Check existence</td><td><code>get.addColumn(cf, &quot;d&quot;)</code> → check if result is empty</td><td>O(1)</td></tr><tr><td>Retrieve all</td><td><code>get.addFamily(cf)</code> → iterate qualifiers</td><td>O(n)</td></tr></tbody></table><p>Since HBase qualifiers are <strong>unique</strong> within the same row and column family, deduplication is naturally guaranteed.</p><p>Example code:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Add elements</span></span><br><span class="line"><span class="type">Put</span> <span class="variable">put</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Put</span>(Bytes.toBytes(<span class="string">&quot;set:user:1001&quot;</span>));</span><br><span class="line">put.addColumn(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>), Bytes.toBytes(<span class="string">&quot;vip&quot;</span>), Bytes.toBytes(<span class="string">&quot;1&quot;</span>));</span><br><span class="line">put.addColumn(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>), Bytes.toBytes(<span class="string">&quot;premium&quot;</span>), Bytes.toBytes(<span class="string">&quot;1&quot;</span>));</span><br><span class="line">table.put(put);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Check if an element exists</span></span><br><span class="line"><span class="type">Get</span> <span class="variable">get</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Get</span>(Bytes.toBytes(<span class="string">&quot;set:user:1001&quot;</span>));</span><br><span class="line">get.addColumn(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>), Bytes.toBytes(<span class="string">&quot;vip&quot;</span>));</span><br><span class="line"><span class="type">boolean</span> <span class="variable">exists</span> <span class="operator">=</span> table.exists(get);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Retrieve the entire Set</span></span><br><span class="line"><span class="type">Get</span> <span class="variable">getAll</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Get</span>(Bytes.toBytes(<span class="string">&quot;set:user:1001&quot;</span>));</span><br><span class="line">getAll.addFamily(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>));</span><br><span class="line"><span class="type">Result</span> <span class="variable">result</span> <span class="operator">=</span> table.get(getAll);</span><br><span class="line">NavigableMap&lt;<span class="type">byte</span>[], <span class="type">byte</span>[]&gt; familyMap = result.getFamilyMap(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>));</span><br><span class="line">Set&lt;String&gt; elements = familyMap.keySet().stream()</span><br><span class="line">    .map(Bytes::toString)</span><br><span class="line">    .collect(Collectors.toSet());</span><br></pre></td></tr></table></figure><h2 id="Limitations"><a href="#Limitations" class="headerlink" title="Limitations"></a>Limitations</h2><ul><li><strong>Qualifier size limit</strong>: Individual qualifiers should not be too large (HBase stores by row, large qualifiers degrade performance)</li><li><strong>Column count per row</strong>: Having too many columns in a single row (millions+) can cause performance issues. If the Set is very large, consider partitioning</li><li><strong>No native sorting</strong>: Qualifiers are sorted in lexicographic order by default. For ordered Sets, you’ll need custom sorting logic</li></ul><h2 id="Extension-Weighted-Set"><a href="#Extension-Weighted-Set" class="headerlink" title="Extension: Weighted Set"></a>Extension: Weighted Set</h2><p>If you need functionality similar to Redis Sorted Set (each element has a score), you can place the score in the value:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rowkey       cf:qualifier     value</span><br><span class="line">rank         cf:Alice         95.5</span><br><span class="line">rank         cf:Bob           87.0</span><br><span class="line">rank         cf:Carol         92.3</span><br></pre></td></tr></table></figure><p>When retrieving, scan all qualifiers and sort by value in the application layer.</p><p>If you’ve encountered similar structured storage needs in HBase data modeling, this approach can serve as a clean reference implementation.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/46290/">https://lichuanyang.top/en/posts/46290/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/46290/</id>
    <link href="https://lichuanyang.top/en/posts/46290/"/>
    <published>2017-04-10T12:42:00.000Z</published>
    <summary>A storage scheme for implementing Set data structures in HBase, addressing HBase's native lack of support for collection types.</summary>
    <title>An Approach to Storing Sets in HBase</title>
    <updated>2026-06-27T03:50:56.854Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="logging" scheme="https://lichuanyang.top/en/tags/logging/"/>
    <content>
      <![CDATA[<h2 id="Complete-Configuration-Example"><a href="#Complete-Configuration-Example" class="headerlink" title="Complete Configuration Example"></a>Complete Configuration Example</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span> ?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">configuration</span> <span class="attr">status</span>=<span class="string">&quot;warn&quot;</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">Appenders</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Console</span> <span class="attr">name</span>=<span class="string">&quot;Console&quot;</span> <span class="attr">target</span>=<span class="string">&quot;SYSTEM_OUT&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">PatternLayout</span> <span class="attr">pattern</span>=<span class="string">&quot;[%p] %d %c %l - %m%n&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">Console</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">RollingFile</span> <span class="attr">name</span>=<span class="string">&quot;activity&quot;</span> <span class="attr">fileName</span>=<span class="string">&quot;/opt/fox.log&quot;</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">filePattern</span>=<span class="string">&quot;/opt/fox.log.%d&#123;yyyy-MM-dd&#125;.gz&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">Policies</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">TimeBasedTriggeringPolicy</span> <span class="attr">interval</span>=<span class="string">&quot;1&quot;</span> <span class="attr">modulate</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">Policies</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">PatternLayout</span> <span class="attr">pattern</span>=<span class="string">&quot;[%p] %d - %m%n&quot;</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">RollingFile</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">RollingFile</span> <span class="attr">name</span>=<span class="string">&quot;fox_err&quot;</span> <span class="attr">fileName</span>=<span class="string">&quot;/opt/fox_err.log&quot;</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">filePattern</span>=<span class="string">&quot;/opt/fox_err..log.%d&#123;yyyy-MM-dd&#125;.gz&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">Policies</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">TimeBasedTriggeringPolicy</span> <span class="attr">interval</span>=<span class="string">&quot;1&quot;</span> <span class="attr">modulate</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">Policies</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">PatternLayout</span> <span class="attr">pattern</span>=<span class="string">&quot;[%p] %d %l - %m%n&quot;</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">RollingFile</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Appenders</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Loggers</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">logger</span> <span class="attr">name</span>=<span class="string">&quot;com.fox&quot;</span> <span class="attr">additivity</span>=<span class="string">&quot;false&quot;</span> <span class="attr">level</span>=<span class="string">&quot;info&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">appender-ref</span> <span class="attr">ref</span>=<span class="string">&quot;activity&quot;</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">appender-ref</span> <span class="attr">ref</span>=<span class="string">&quot;activity_err&quot;</span> <span class="attr">level</span>=<span class="string">&quot;error&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">logger</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Root</span> <span class="attr">level</span>=<span class="string">&quot;error&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">AppenderRef</span> <span class="attr">ref</span>=<span class="string">&quot;Console&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">Root</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Loggers</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="Key-Differences-from-Log4j-1-x"><a href="#Key-Differences-from-Log4j-1-x" class="headerlink" title="Key Differences from Log4j 1.x"></a>Key Differences from Log4j 1.x</h2><p>Compared to Log4j, there are several major changes:</p><p>First, the overall structure has changed significantly — appenders and loggers are now each organized under their own root nodes.</p><p>The XML node names have also been redesigned to use descriptive names directly, instead of the previous <code>appender xxx=&quot;xxx&quot;</code> and <code>param xxx=&quot;xxx&quot;</code> format.</p><p>Additionally, certain attributes such as <code>fileName</code> can only be configured as node attributes, not as child nodes as was possible in Log4j.</p><p>Furthermore, Log4j2 supports compression when archiving. By specifying a compressed file extension such as <code>.gz</code> or <code>.zip</code> in the <code>filePattern</code> attribute of the <code>RollingFile</code> node, Log4j2 will automatically select the appropriate compression algorithm.</p><h2 id="Dependencies-and-Migration"><a href="#Dependencies-and-Migration" class="headerlink" title="Dependencies and Migration"></a>Dependencies and Migration</h2><p>That covers the main differences I’ve discovered so far. By importing this XML configuration along with the <code>log4j-core</code> and <code>log4j-api</code> packages, you can start using Log4j2. Additionally, if needed, you can use <code>log4j-slf4j-impl</code>, <code>log4j-jcl</code>, and <code>log4j-1.2-api</code> to achieve compatibility with SLF4J, JCL, and Log4j respectively.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/41673/">https://lichuanyang.top/en/posts/41673/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/41673/</id>
    <link href="https://lichuanyang.top/en/posts/41673/"/>
    <published>2017-04-10T12:23:00.000Z</published>
    <summary>Practical Log4j2 XML configuration examples, comparing the major changes from Log4j: new node naming, attribute configuration approach, and archive compression support.</summary>
    <title>Log4j2 XML Configuration Examples and Differences from Log4j</title>
    <updated>2026-06-27T02:18:07.397Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <content>
      <![CDATA[<h2 id="Problem-Statement"><a href="#Problem-Statement" class="headerlink" title="Problem Statement"></a>Problem Statement</h2><p>In Java, there are several ways to compare whether two Lists have the same values regardless of order, such as sorting followed by using <code>equals</code>, or executing <code>containsAll</code> in both directions, etc. All of these methods require us to implement the <code>equals</code> and <code>hashCode</code> methods for the element classes in the list.</p><p>However, there are special cases where it’s not convenient for us to implement the <code>equals</code> method of a class — for example, when it comes from an ancient third-party JAR package where modifying the code could bring many unknown issues. What should we do in such situations?</p><h2 id="Comparison-Approach-Without-equals"><a href="#Comparison-Approach-Without-equals" class="headerlink" title="Comparison Approach Without equals"></a>Comparison Approach Without equals</h2><p>Actually, it’s quite simple. The all-powerful Apache Commons already thought of this, so they added externally supplied <code>equals</code> and <code>hashCode</code> methods in <code>commons-collections4</code>. Even the <code>equals</code> and <code>hashCode</code> methods themselves don’t need to be written by us — they can be implemented using the <code>commons-lang</code> package. The specific code is as follows:</p><h2 id="Using-Third-Party-Tools"><a href="#Using-Third-Party-Tools" class="headerlink" title="Using Third-Party Tools"></a>Using Third-Party Tools</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;commons-collections4&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;4.1&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;3.5&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">public static &lt;T&gt; boolean isEqualCollection(Collection&lt;T&gt; l1, Collection&lt;T&gt; l2, final String... exludedFields) &#123;</span><br><span class="line">    Equator&lt;T&gt; equator = generateEquator(exludedFields);</span><br><span class="line">    return CollectionUtils.isEqualCollection(l1, l2, equator);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">private static  &lt;T&gt; Equator&lt;T&gt; generateEquator(final String... exludedFields) &#123;</span><br><span class="line">    Equator&lt;T&gt; equator = new Equator&lt;T&gt;() &#123;</span><br><span class="line">        @Override</span><br><span class="line">        public boolean equate(T o1, T o2) &#123;</span><br><span class="line">            if (o1 == null &amp;&amp; o2 == null) &#123;</span><br><span class="line">                return true;</span><br><span class="line">            &#125;</span><br><span class="line">            if (o1 == null || o2 == null) &#123;</span><br><span class="line">                return false;</span><br><span class="line">            &#125;</span><br><span class="line">            if (o1.getClass() != o2.getClass()) &#123;</span><br><span class="line">                return false;</span><br><span class="line">            &#125;</span><br><span class="line">            return EqualsBuilder.reflectionEquals(o1, o2, exludedFields);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        @Override</span><br><span class="line">        public int hash(T o) &#123;</span><br><span class="line">            return HashCodeBuilder.reflectionHashCode(o, exludedFields);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    return equator;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Source: <a href="https://lichuanyang.top/en/posts/28720/">https://lichuanyang.top/en/posts/28720/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/28720/</id>
    <link href="https://lichuanyang.top/en/posts/28720/"/>
    <published>2017-04-10T11:57:00.000Z</published>
    <summary>Use Apache Commons utility methods to compare two Lists' values without modifying the equals method of third-party classes.</summary>
    <title>Comparing Java Lists Without Implementing equals</title>
    <updated>2026-06-27T03:53:05.206Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Database" scheme="https://lichuanyang.top/en/categories/Database/"/>
    <category term="redis" scheme="https://lichuanyang.top/en/tags/redis/"/>
    <category term="lua" scheme="https://lichuanyang.top/en/tags/lua/"/>
    <content>
      <![CDATA[<p>In Redis’s Hash operations, <code>HSETNX</code> ensures that a field is only written when it does not already exist. However, <code>HMSET</code> (which sets multiple fields in batch) has no corresponding <code>HMSETNX</code> command. If you need to batch-initialize a cache without overwriting existing data, issuing N <code>HSETNX</code> commands would require N network round-trips — terrible performance in batch scenarios.</p><p>This can be solved elegantly with a Lua script — pushing the logic to the Redis server side and requiring only a single network round-trip.</p><span id="more"></span><h2 id="Problem-Background"><a href="#Problem-Background" class="headerlink" title="Problem Background"></a>Problem Background</h2><p>Imagine a user configuration caching scenario: when a new user accesses the system for the first time, a set of default configurations needs to be initialized in a Redis Hash. However, if the user already has some configurations, those should not be overwritten.</p><p>Using <code>HSETNX</code> one by one would certainly work, but if there are 20 configuration items, that means 20 Redis round-trips. Under high concurrency, this becomes a bottleneck.</p><p><code>HMSET</code> can set multiple fields at once, but it <strong>unconditionally overwrites</strong> existing values — which doesn’t meet the requirement.</p><h2 id="Lua-Script-Solution"><a href="#Lua-Script-Solution" class="headerlink" title="Lua Script Solution"></a>Lua Script Solution</h2><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> key</span><br><span class="line"><span class="keyword">for</span> i, j <span class="keyword">in</span> <span class="built_in">ipairs</span>(ARGV)</span><br><span class="line"><span class="keyword">do</span></span><br><span class="line">    <span class="keyword">if</span> i % <span class="number">2</span> == <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line">        redis.call(<span class="string">&#x27;hsetnx&#x27;</span>, KEYS[<span class="number">1</span>], key, j)</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        key = j</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><p><strong>Script Logic</strong>:</p><p>The <code>ARGV</code> format is identical to <code>HMSET</code>: <code>[field1, value1, field2, value2, ...]</code>. The script iterates through the parameter list — odd positions (1, 3, 5…) are stored as <code>key</code>, while even positions (2, 4, 6…) trigger an <code>HSETNX</code> call.</p><p>Since Redis executes Lua scripts atomically, the entire process cannot be interrupted by other commands.</p><h2 id="Java-Invocation-Jedis"><a href="#Java-Invocation-Jedis" class="headerlink" title="Java Invocation (Jedis)"></a>Java Invocation (Jedis)</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Script string (can be stored in a constant)</span></span><br><span class="line"><span class="type">String</span> <span class="variable">HMSETNX_SCRIPT</span> <span class="operator">=</span> </span><br><span class="line">    <span class="string">&quot;local key\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;for i, j in ipairs(ARGV)\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;do\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;    if i % 2 == 0 then\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;        redis.call(&#x27;hsetnx&#x27;, KEYS[1], key, j)\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;    else\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;        key = j\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;    end\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;end\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;return 1&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Invocation</span></span><br><span class="line"><span class="type">Jedis</span> <span class="variable">jedis</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Jedis</span>(<span class="string">&quot;localhost&quot;</span>, <span class="number">6379</span>);</span><br><span class="line">List&lt;String&gt; keys = Collections.singletonList(<span class="string">&quot;user:config:1001&quot;</span>);</span><br><span class="line">List&lt;String&gt; args = Arrays.asList(<span class="string">&quot;theme&quot;</span>, <span class="string">&quot;dark&quot;</span>, <span class="string">&quot;lang&quot;</span>, <span class="string">&quot;zh&quot;</span>, <span class="string">&quot;timezone&quot;</span>, <span class="string">&quot;UTC+8&quot;</span>);</span><br><span class="line"></span><br><span class="line">jedis.eval(HMSETNX_SCRIPT, keys, args);</span><br></pre></td></tr></table></figure><p><strong>Parameter Explanation</strong>:</p><ul><li><code>keys</code>: Contains only one element — the key of the Hash table itself</li><li><code>args</code>: Arranged in <code>field1, value1, field2, value2...</code> order</li><li><code>eval()</code> has two overloads: <code>String</code> version and <code>byte[]</code> version — the difference is whether parameters need serialization</li></ul><h2 id="Performance-Optimization-EVALSHA"><a href="#Performance-Optimization-EVALSHA" class="headerlink" title="Performance Optimization: EVALSHA"></a>Performance Optimization: EVALSHA</h2><p>Every <code>EVAL</code> call transmits the full script content. For fixed scripts, <code>EVALSHA</code> can significantly reduce network overhead:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Compute the script&#x27;s SHA1</span></span><br><span class="line"><span class="type">String</span> <span class="variable">sha</span> <span class="operator">=</span> jedis.scriptLoad(HMSETNX_SCRIPT);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Subsequent calls use the SHA1 directly</span></span><br><span class="line">jedis.evalsha(sha, keys, args);</span><br></pre></td></tr></table></figure><p><code>EVALSHA</code> only transmits a few dozen bytes (the SHA1 value). The script itself is only transmitted once during the initial <code>SCRIPT LOAD</code>.</p><h2 id="Hmsetnx-vs-Per-field-Hsetnx-Performance-Comparison"><a href="#Hmsetnx-vs-Per-field-Hsetnx-Performance-Comparison" class="headerlink" title="Hmsetnx vs Per-field Hsetnx: Performance Comparison"></a>Hmsetnx vs Per-field Hsetnx: Performance Comparison</h2><table><thead><tr><th>Approach</th><th>Network Round-trips</th><th>Atomicity</th><th>Complexity</th></tr></thead><tbody><tr><td>Per-field <code>HSETNX</code></td><td>N</td><td>No (non-transactional)</td><td>Lowest</td></tr><tr><td>Lua script <code>EVAL</code></td><td>1</td><td>Yes (Lua atomic execution)</td><td>Medium</td></tr><tr><td>Lua script <code>EVALSHA</code></td><td>1</td><td>Yes</td><td>Medium</td></tr></tbody></table><p>For a scenario with 20 fields, the Lua approach eliminates 19 network round-trips — the effect is dramatic under high concurrency.</p><h2 id="Notes"><a href="#Notes" class="headerlink" title="Notes"></a>Notes</h2><ul><li><strong>Cluster Mode</strong>: If Redis is deployed in Cluster mode, ensure <code>KEYS[1]</code> (the Hash key) falls in the same slot. Use hash tags to guarantee this (e.g., <code>{user:1001}:config</code>)</li><li><strong>Big Key</strong>: If the Hash contains a huge number of fields, <code>HSETNX</code> itself is O(1) per operation, but be mindful of the total key size</li><li><strong>Script Length</strong>: Redis enforces a time limit on Lua script execution (<code>lua-time-limit</code>), but this script just loops through O(n) <code>HSETNX</code> calls and won’t trigger a timeout</li></ul><hr><p>Source: <a href="https://lichuanyang.top/en/posts/63756/">https://lichuanyang.top/en/posts/63756/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/63756/</id>
    <link href="https://lichuanyang.top/en/posts/63756/"/>
    <published>2017-04-06T09:54:00.000Z</published>
    <summary>Implementing Redis's missing hmsetnx command with Lua scripts and Jedis, to avoid overwriting existing data when batch operating on Hash tables.</summary>
    <title>Implementing Redis hmsetnx Command Using Lua Scripts and Jedis</title>
    <updated>2026-06-27T03:51:55.442Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="monitoring" scheme="https://lichuanyang.top/en/tags/monitoring/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="hawtio" scheme="https://lichuanyang.top/en/tags/hawtio/"/>
    <content>
      <![CDATA[<p>Hawtio (hawt.io) is an open-source monitoring system that provides multiple startup methods. It can run as a standalone JAR or WAR package and remotely connect to other applications for monitoring, or it can be directly embedded into our own applications.</p><span id="more"></span><p>This article introduces how to embed Hawtio in a standalone Java application, corresponding to the official documentation (<a href="http://hawt.io/getstarted/index.html">http://hawt.io/getstarted/index.html</a>) section “Using hawtio inside a stand alone Java application”. However, that section of the documentation has quite a few issues — if you only follow that section, you’ll encounter various problems.</p><p>Below are the detailed steps.</p><h2 id="Adding-Dependencies"><a href="#Adding-Dependencies" class="headerlink" title="Adding Dependencies"></a>Adding Dependencies</h2><p>In addition to the <code>hawtio-embedded</code> mentioned in the official documentation, the <code>hawtio-insight-log</code>, <code>hawtio-core</code>, and <code>hawtio-web</code> packages are all required. We use the latest versions available at the time.</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.hawt<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>hawtio-embedded<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.hawt<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>hawtio-insight-log<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.5.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.hawt<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>hawtio-core<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.hawt<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>hawtio-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Downloading-the-WAR-Package"><a href="#Downloading-the-WAR-Package" class="headerlink" title="Downloading the WAR Package"></a>Downloading the WAR Package</h2><p>Also from the official getting started documentation (<a href="http://hawt.io/getstarted/index.html">http://hawt.io/getstarted/index.html</a>), download the <code>hawtio-default.war</code> package and place it anywhere. The WAR file name can be changed to anything you like.</p><h2 id="Code"><a href="#Code" class="headerlink" title="Code"></a>Code</h2><p>The basic code is very simple if no additional configuration is needed:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Main</span> <span class="variable">main</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Main</span>();</span><br><span class="line">main.setWar(<span class="string">&quot;hawtio.war&quot;</span>);</span><br><span class="line">main.run();</span><br></pre></td></tr></table></figure><p>The <code>main.setWar</code> method configures the specific path to the WAR package. It can be inside or outside the project directory. Note that it is NOT “the directory path containing the Hawtio WAR” as stated in the official documentation.</p><p>Then run <code>main.run()</code> to start Hawtio.</p><h2 id="Authentication"><a href="#Authentication" class="headerlink" title="Authentication"></a>Authentication</h2><p>Newer versions of Hawtio require a password by default. For simplicity, you can add a JVM parameter to disable authentication:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-Dhawtio.authenticationEnabled=false</span><br></pre></td></tr></table></figure><p>Example code can be found on GitHub (<a href="https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java">https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java</a>). It can be downloaded and run directly, but you’ll need a local ActiveMQ instance. The example demonstrates using Hawtio to monitor Apache Camel. After startup, the Camel tab will appear directly in the Hawtio web interface, making it very convenient to use.</p><p>Source: <a href="https://lichuanyang.top/en/posts/11299/">https://lichuanyang.top/en/posts/11299/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/11299/</id>
    <link href="https://lichuanyang.top/en/posts/11299/"/>
    <published>2017-04-01T11:42:00.000Z</published>
    <summary>A complete guide to embedding Hawtio monitoring in a standalone Java application for viewing JMX and other monitoring data directly.</summary>
    <title>Embedding Hawtio Monitoring in a Standalone Java Application</title>
    <updated>2026-06-27T03:53:05.205Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="serialization" scheme="https://lichuanyang.top/en/tags/serialization/"/>
    <category term="kryo" scheme="https://lichuanyang.top/en/tags/kryo/"/>
    <category term="protobuf" scheme="https://lichuanyang.top/en/tags/protobuf/"/>
    <category term="json" scheme="https://lichuanyang.top/en/tags/json/"/>
    <content>
      <![CDATA[<div class="markdown_views"><h2 id="Why-Compare-Serialization-Solutions"><a href="#Why-Compare-Serialization-Solutions" class="headerlink" title="Why Compare Serialization Solutions"></a>Why Compare Serialization Solutions</h2><p>For serializing a Java object, I wanted to test the differences in time and space performance between using JSON and using general-purpose serialization tools.</p><p>For JSON, I chose fastjson.</p><p>For serialization tools, I used protostuff and kryo. Why not protobuf? Because for an existing Java class with hundreds of properties, creating a matching proto file feels somewhat inhumane. Protostuff is an improved version of protobuf — it can directly serialize a Java object, and the usage is somewhat similar to kryo, without so many intermediate steps as protobuf. As for others like Hessian and Java’s built-in serialization, it’s said that their performance is much worse than kryo and protobuf, so I didn’t bother testing them.</p><h2 id="Time-Performance-Comparison"><a href="#Time-Performance-Comparison" class="headerlink" title="Time Performance Comparison"></a>Time Performance Comparison</h2><p>After a quick test, I found the performance gap was quite obvious, so I felt there was no need for a detailed evaluation. Let me share a snippet of the log so you can get a feel for it.</p><pre><code>fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;555805&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;227375&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;78950&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;130662&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;201716&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;230533&lt;/span&gt;fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;727915&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;378958&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;94739&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;154346&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;373432&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs_number&quot;&gt;219085&lt;/span&gt;fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;804892&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;392380&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;220664&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;243560&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;360010&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs_number&quot;&gt;132241&lt;/span&gt;fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;601991&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;244349&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;80924&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs_number&quot;&gt;241191&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs_number&quot;&gt;230928&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs_number&quot;&gt;127109&lt;/span&gt;</code></pre><p>The cost was measured using <code>System.nanoTime()</code>. All three were tested with their default configurations without any additional settings.</p><h2 id="Space-Performance-Comparison"><a href="#Space-Performance-Comparison" class="headerlink" title="Space Performance Comparison"></a>Space Performance Comparison</h2><p>In terms of serialized output size, kryo is slightly smaller than protostuff, and both are significantly smaller than JSON. This is easy to understand — after all, JSON strings are human-readable, so you can’t expect too much from them.</p><p>As for serialization and deserialization time, protostuff outperforms kryo, which in turn outperforms fastjson, and the differences are quite significant.</p><h2 id="Serialization-Solution-Recommendations"><a href="#Serialization-Solution-Recommendations" class="headerlink" title="Serialization Solution Recommendations"></a>Serialization Solution Recommendations</h2><p>So the conclusion is: if you don’t have extremely stringent requirements on space, protostuff might be the best choice. Compared to kryo, protostuff has an additional advantage: if the Java class gains new fields during the time between serialization and deserialization (which is unavoidable in real-world business), kryo becomes useless. However, protostuff can still work normally as long as new fields are added at the end of the class and you’re using a Sun-based JDK. Therefore, if serialization is used in scenarios like caching where objects need to be stored for a long time, protostuff is really the only viable option.</p><p>Of course, if you have requirements for readability or similar needs, JSON is the way to go.</p></div><p>&nbsp;</p><p>Source: <a href="https://lichuanyang.top/en/posts/57802/">https://lichuanyang.top/en/posts/57802/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/57802/</id>
    <link href="https://lichuanyang.top/en/posts/57802/"/>
    <published>2017-03-02T10:10:00.000Z</published>
    <summary>Comparing the time and space performance of mainstream Java serialization solutions (kryo, protobuf, protostuff, fastjson) with real benchmark data to help you choose the most suitable serialization tool.</summary>
    <title>Java Serialization Performance Comparison: kryo/protobuf/protostuff vs Json</title>
    <updated>2026-06-27T02:17:57.600Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="redis" scheme="https://lichuanyang.top/en/tags/redis/"/>
    <category term="zookeeper" scheme="https://lichuanyang.top/en/tags/zookeeper/"/>
    <category term="distributed" scheme="https://lichuanyang.top/en/tags/distributed/"/>
    <content>
      <![CDATA[<p>This article covers usage only, no theory. Simple and straightforward.</p><p>A distributed lock is, as the name suggests, a lock for distributed systems, used in scenarios where multiple instances need coordination. For example, if a service is deployed on multiple machines and may operate on the same record in a database, a distributed lock becomes necessary.</p><p>There are many ways to implement distributed locks. Generally, a lock requires an entity to represent it — when acquiring the lock, this entity is created; when releasing the lock, the corresponding entity is deleted. Additionally, a lock expiration strategy is typically needed to handle situations where a lock cannot be released due to abnormal conditions.</p><p>Both Zookeeper and Redis are commonly used to implement distributed locks. Below is a brief introduction to using each approach.</p><h2 id="Zookeeper-based-Distributed-Lock"><a href="#Zookeeper-based-Distributed-Lock" class="headerlink" title="Zookeeper-based Distributed Lock"></a>Zookeeper-based Distributed Lock</h2><p>If using Zookeeper, it is recommended to use the Curator client directly.</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.curator<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>curator-recipes<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.9.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Curator provides an <code>InterProcessSemaphoreMutex</code> class that serves as a distributed lock.</p><p>The implementation principle is straightforward: when creating a lock, a node is created at an agreed-upon path to serve as the lock entity; when releasing the lock, this node is deleted.</p><p>Code example snippet:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">InterProcessSemaphoreMutex lock;</span><br><span class="line">lock.acquire(...);  <span class="comment">// acquire the lock</span></span><br><span class="line">...</span><br><span class="line">lock.release();     <span class="comment">// release the lock</span></span><br></pre></td></tr></table></figure><p>For a detailed usage example, refer to <a href="https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/zklock/InterProcessMutexExample.java">https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/zklock/InterProcessMutexExample.java</a></p><h2 id="Redis-based-Distributed-Lock"><a href="#Redis-based-Distributed-Lock" class="headerlink" title="Redis-based Distributed Lock"></a>Redis-based Distributed Lock</h2><p>Similarly, a third-party Redis client Redisson is recommended: <a href="https://github.com/redisson/redisson">https://github.com/redisson/redisson</a>. Redisson is not as well-known as Curator, but it is also an excellent open-source tool that supports various cluster configurations and data structures.</p><p>The principle of a Redis lock is simple: when acquiring the lock, a new key is created; when releasing the lock, the key is deleted.</p><p>For code examples, refer to <a href="https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/redislock/ValuelockExample.java">https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/redislock/ValuelockExample.java</a></p><p>Source: <a href="https://lichuanyang.top/en/posts/48104/">https://lichuanyang.top/en/posts/48104/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/48104/</id>
    <link href="https://lichuanyang.top/en/posts/48104/"/>
    <published>2017-02-20T11:21:00.000Z</published>
    <summary>Getting started with Java distributed locks in practice, covering distributed lock implementations based on Zookeeper and Redis — usage only, no theory.</summary>
    <title>Java Distributed Lock Practice for Beginners</title>
    <updated>2026-06-27T03:51:55.448Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Algorithm" scheme="https://lichuanyang.top/en/categories/Algorithm/"/>
    <category term="algorithm" scheme="https://lichuanyang.top/en/tags/algorithm/"/>
    <category term="leetcode" scheme="https://lichuanyang.top/en/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="Problem"><a href="#Problem" class="headerlink" title="Problem"></a>Problem</h2><p>Given a string, find the length of the longest substring without repeating characters.</p><p>Examples:</p><p>Given “abcabcbb”, the answer is “abc”, which the length is 3.</p><p>Given “bbbbb”, the answer is “b”, with the length of 1.</p><p>Given “pwwkew”, the answer is “wke”, with the length of 3. Note that the answer must be a substring, “pwke” is a subsequence and not a substring.</p><p>In other words, given a string, output the length of the longest substring without repeating characters.</p><h2 id="Approach"><a href="#Approach" class="headerlink" title="Approach"></a>Approach</h2><p>The problem can be solved in a single pass with O(n) complexity. The main idea is to:</p><ol><li>Record the last position where each character appeared</li><li>Maintain a substring (tracked by <code>start</code> and <code>end</code> indices) that contains no repeating characters up to the current position</li></ol><p>During traversal, <code>start</code> and <code>end</code> are continuously updated based on whether the character at the current position has been seen before and its last occurrence position.</p><h2 id="Code"><a href="#Code" class="headerlink" title="Code"></a>Code</h2><p>The code can be viewed on GitHub: <a href="https://github.com/lcy362/Algorithms/tree/master/src/main/java/com/mallow/algorithm">https://github.com/lcy362/Algorithms/tree/master/src/main/java/com/mallow/algorithm</a></p><pre><code>&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; java.util.HashMap;&lt;span class=&quot;hljs-javadoc&quot;&gt;/** * leetcode 3 * https://leetcode.com/problems/longest-substring-without-repeating-characters/ * Created by lcy on 2017/2/15. */&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-class&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;LongestSubstringNotRepeat&lt;/span&gt; {&lt;/span&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;lengthOfLongestSubstring&lt;/span&gt;(String s) {        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (s.length() &amp;lt;= &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;) {            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; s.length();        }        HashMap&amp;lt;Character, Integer&amp;gt; charPos = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; HashMap&amp;lt;&amp;gt;();        &lt;span class=&quot;hljs-keyword&quot;&gt;char&lt;/span&gt;[] chars = s.toCharArray();        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; len = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; max = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; start = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; end = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; chars.length; i++) {            &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (charPos.containsKey(chars[i])) {                &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; tempstart = charPos.get(chars[i]) + &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;                &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (tempstart &amp;gt; start) {                    start = tempstart;                }                end++;                len = end - start;            } &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; {                len++;                end++;            }            charPos.put(chars[i], i);            &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (len &amp;gt; max) {                max = len;            }        }        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; max;    }    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;main&lt;/span&gt;(String args[]) {        LongestSubstringNotRepeat l = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; LongestSubstringNotRepeat();        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;abcabcbb&quot;&lt;/span&gt;));        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;bbbbb&quot;&lt;/span&gt;));        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;pwwkew&quot;&lt;/span&gt;));        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;abba&quot;&lt;/span&gt;));    }</code></pre><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/43423/</id>
    <link href="https://lichuanyang.top/en/posts/43423/"/>
    <published>2017-02-15T12:34:00.000Z</published>
    <summary>Detailed solution for LeetCode Problem 3: finding the length of the longest substring without repeating characters using the sliding window approach.</summary>
    <title>LeetCode Problem 3: Longest Substring Without Repeating Characters</title>
    <updated>2026-06-27T03:49:47.414Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="kafka" scheme="https://lichuanyang.top/en/tags/kafka/"/>
    <category term="growth" scheme="https://lichuanyang.top/en/tags/growth/"/>
    <content>
      <![CDATA[<h2 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h2><p>Our company needs to receive a large amount of external data from various sources, including IBM MQ, ActiveMQ, Redis pub&#x2F;sub, and more. To funnel all this data into our internal AMQ&#x2F;Kafka, we previously ran a large number of separate processes, which were quite complex to manage. Recently, we consolidated these processes using Apache Camel.</p><p>A few hours after deployment, Kafka disk space alerts started firing. We initially determined that this was caused by the new deployment.</p><h2 id="Investigation-Process"><a href="#Investigation-Process" class="headerlink" title="Investigation Process"></a>Investigation Process</h2><p>Since I wasn’t very familiar with Kafka — I only knew how to use it — the investigation took some detours.</p><p>Because Camel’s documentation is quite incomplete, I initially relied on reading source code and guessing when configuring parameters. So I first suspected that the compression parameter <code>compressionCodec=gzip</code> wasn’t working.</p><p>I conducted a simple test (don’t ask why I didn’t test from the beginning). I created a single-partition test topic, then sent identical data using both gzip and none compression modes, observing the change in Kafka log file sizes. The compression was indeed effective.</p><p>I then compared it with using the Kafka API directly and found a significant difference — even with compression enabled, Camel-Kafka consumed several times more space than using the API. I examined the source code on both sides and found the lowest-level code was identical, so I suspected some parameter was misconfigured.</p><p>Then I noticed a detail: with Camel, the log size was linearly proportional to the message count — e.g., one message took 1 byte, ten messages took 10 bytes. But with the API, one message took 1 byte, and sending ten messages consecutively might only take about 2 bytes.</p><p>This raised the suspicion that Kafka might have a batch sending mechanism. After consulting a colleague responsible for Kafka, this turned out to be exactly the cause. The key difference was whether synchronous sending was used — with synchronous sending, there’s no batching. With batch sending, messages in a batch are compressed together, while with individual sending, each message is compressed separately. As we know, gzip compression is very ineffective on very small files and can even produce output larger than the input. After further testing, I confirmed this was the issue. This parameter was wrapped inside Kafka’s client interface by a colleague, which caused me to miss it when modifying parameters based on existing code.</p><h2 id="Reflections"><a href="#Reflections" class="headerlink" title="Reflections"></a>Reflections</h2><p>Looking back, this problem was quite basic. With a better understanding of Kafka, it could have been avoided.</p><p>First, I hadn’t done a comprehensive study of Kafka — I only learned how to use it and had a rough idea of what it was, without understanding many of its fundamental mechanisms. When using an open-source tool, it’s important to do a thorough study of it. While it’s not realistic to deeply study the source code of every tool, a systematic overview of the mechanisms these tools offer doesn’t take much time.</p><p>Second, companies typically wrap various component access tools for efficiency. It’s best to also understand how these wrappers are implemented, otherwise you might unknowingly fall into a trap.</p><p>There’s still a long way to go.</p><p>Source: <a href="https://lichuanyang.top/en/posts/40931/">https://lichuanyang.top/en/posts/40931/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/40931/</id>
    <link href="https://lichuanyang.top/en/posts/40931/"/>
    <published>2016-12-29T09:21:00.000Z</published>
    <summary>An investigation into a Kafka disk space usage spike, diving into Kafka's data compression mechanism and batch sending strategies.</summary>
    <title>Investigating a Kafka Disk Usage Spike: Data Compression, Batch Sending, and More</title>
    <updated>2026-06-08T07:50:49.119Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/en/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <category term="java-integer" scheme="https://lichuanyang.top/en/tags/java-integer/"/>
    <category term="java-integer caching" scheme="https://lichuanyang.top/en/tags/java-integer-caching/"/>
    <content>
      <![CDATA[<p>Java’s <code>Integer</code> caching mechanism is a frequently asked interview topic and a common source of pitfalls in daily development. To understand it, let’s start with a counterintuitive experiment.</p><span id="more"></span><h2 id="The-Curious-Case-of-128"><a href="#The-Curious-Case-of-128" class="headerlink" title="The Curious Case of 128"></a>The Curious Case of 128</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Integer</span> <span class="variable">a</span> <span class="operator">=</span> <span class="number">148</span>;</span><br><span class="line"><span class="type">Integer</span> <span class="variable">b</span> <span class="operator">=</span> <span class="number">148</span>;</span><br><span class="line">System.out.println(a == b);  <span class="comment">// false — as expected, two different objects</span></span><br><span class="line"></span><br><span class="line"><span class="type">Integer</span> <span class="variable">c</span> <span class="operator">=</span> <span class="number">48</span>;</span><br><span class="line"><span class="type">Integer</span> <span class="variable">d</span> <span class="operator">=</span> <span class="number">48</span>;</span><br><span class="line">System.out.println(c == d);  <span class="comment">// true — wait, why?</span></span><br></pre></td></tr></table></figure><p>The same <code>==</code> comparison returns false for 148 but true for 48. The reason lies in <strong>Integer caching</strong>.</p><h2 id="IntegerCache-Source-Code-Analysis"><a href="#IntegerCache-Source-Code-Analysis" class="headerlink" title="IntegerCache Source Code Analysis"></a>IntegerCache Source Code Analysis</h2><p>When Java compiles <code>Integer a = 48</code>, what actually executes is:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Integer</span> <span class="variable">a</span> <span class="operator">=</span> Integer.valueOf(<span class="number">48</span>);</span><br></pre></td></tr></table></figure><p>Let’s look at the source of <code>Integer.valueOf()</code> (JDK 8):</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Integer <span class="title function_">valueOf</span><span class="params">(<span class="type">int</span> i)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (i &gt;= IntegerCache.low &amp;&amp; i &lt;= IntegerCache.high)</span><br><span class="line">        <span class="keyword">return</span> IntegerCache.cache[i + (-IntegerCache.low)];</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(i);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The core logic: if the parameter value falls within the cache range (default -128 to 127), it returns the same instance from the cache array; values outside the range trigger <code>new Integer(i)</code>.</p><p>This is why <code>48</code> (within the cache range) returns <code>true</code> for <code>==</code>, while <code>148</code> (outside the range) creates a new object each time and returns <code>false</code>.</p><p><code>IntegerCache</code> is a private static inner class:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">IntegerCache</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">low</span> <span class="operator">=</span> -<span class="number">128</span>;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> high;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> Integer cache[];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        <span class="comment">// high can be configured via a JVM parameter</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">h</span> <span class="operator">=</span> <span class="number">127</span>;</span><br><span class="line">        <span class="type">String</span> <span class="variable">integerCacheHighPropValue</span> <span class="operator">=</span></span><br><span class="line">            sun.misc.VM.getSavedProperty(<span class="string">&quot;java.lang.Integer.IntegerCache.high&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (integerCacheHighPropValue != <span class="literal">null</span>) &#123;</span><br><span class="line">            h = Math.min(Integer.parseInt(integerCacheHighPropValue), Integer.MAX_VALUE - <span class="number">129</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        high = h;</span><br><span class="line"></span><br><span class="line">        cache = <span class="keyword">new</span> <span class="title class_">Integer</span>[(high - low) + <span class="number">1</span>];</span><br><span class="line">        <span class="type">int</span> <span class="variable">j</span> <span class="operator">=</span> low;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">k</span> <span class="operator">=</span> <span class="number">0</span>; k &lt; cache.length; k++)</span><br><span class="line">            cache[k] = <span class="keyword">new</span> <span class="title class_">Integer</span>(j++);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>You can adjust the cache upper bound via a JVM parameter</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -Djava.lang.Integer.IntegerCache.high=500 MyApp</span><br></pre></td></tr></table></figure><h2 id="Caching-in-Other-Wrapper-Classes"><a href="#Caching-in-Other-Wrapper-Classes" class="headerlink" title="Caching in Other Wrapper Classes"></a>Caching in Other Wrapper Classes</h2><table><thead><tr><th>Wrapper Class</th><th>Cache Range</th><th>Notes</th></tr></thead><tbody><tr><td><code>Boolean</code></td><td><code>TRUE</code> &#x2F; <code>FALSE</code></td><td>Only two values</td></tr><tr><td><code>Byte</code></td><td>-128 ~ 127</td><td>Entire range cached</td></tr><tr><td><code>Short</code></td><td>-128 ~ 127</td><td>Same as Integer</td></tr><tr><td><code>Long</code></td><td>-128 ~ 127</td><td>Same as Integer</td></tr><tr><td><code>Character</code></td><td>0 ~ 127</td><td>ASCII range</td></tr><tr><td><code>Float</code></td><td><strong>None</strong></td><td>Caching makes little sense for floating-point</td></tr><tr><td><code>Double</code></td><td><strong>None</strong></td><td>Same as Float</td></tr></tbody></table><h2 id="Three-Common-Pitfall-Scenarios"><a href="#Three-Common-Pitfall-Scenarios" class="headerlink" title="Three Common Pitfall Scenarios"></a>Three Common Pitfall Scenarios</h2><h3 id="1-HashMap-Keys"><a href="#1-HashMap-Keys" class="headerlink" title="1. HashMap Keys"></a>1. HashMap Keys</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Map&lt;Integer, String&gt; map = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">map.put(<span class="number">128</span>, <span class="string">&quot;hello&quot;</span>);</span><br><span class="line">map.get(<span class="number">128</span>);  <span class="comment">// &quot;hello&quot; — even though == differs, equals still matches, so HashMap works fine</span></span><br></pre></td></tr></table></figure><p>HashMap uses <code>equals()</code> and <code>hashCode()</code>, unaffected by <code>==</code>. But if you use <code>IdentityHashMap</code>, the cache boundary becomes an invisible source of bugs.</p><h3 id="2-Integer-Comparisons-in-for-Loops"><a href="#2-Integer-Comparisons-in-for-Loops" class="headerlink" title="2. Integer Comparisons in for Loops"></a>2. Integer Comparisons in for Loops</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ Bad practice</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">Integer</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">200</span>; i++) &#123;</span><br><span class="line">    <span class="comment">// i == some Integer ... may cause issues</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ Good practice</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">200</span>; i++) &#123;</span><br><span class="line">    <span class="comment">// Use primitive types — clean and reliable</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-Lock-Objects"><a href="#3-Lock-Objects" class="headerlink" title="3. Lock Objects"></a>3. Lock Objects</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ Dangerous! Values below 128 return the same object each time,</span></span><br><span class="line"><span class="comment">// meaning you&#x27;re using the same lock</span></span><br><span class="line"><span class="keyword">synchronized</span> (Integer.valueOf(<span class="number">10</span>)) &#123;</span><br><span class="line">    <span class="comment">// If you don&#x27;t know about caching, you might think this is a fresh lock each time</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Why-Is-the-Cache-Range-128-to-127"><a href="#Why-Is-the-Cache-Range-128-to-127" class="headerlink" title="Why Is the Cache Range -128 to 127?"></a>Why Is the Cache Range -128 to 127?</h2><p>This is the signed byte range. Statistically, this interval covers the most commonly used integer values in everyday programming (loop counters, array indices, small constants, etc.). The <em>Java Language Specification</em> §5.1.7 explicitly mandates this behavior — it’s not an implementation detail; it’s guaranteed by the specification.</p><h2 id="Best-Practices"><a href="#Best-Practices" class="headerlink" title="Best Practices"></a>Best Practices</h2><ol><li><strong>Always use <code>equals()</code> when comparing Integer values</strong>, not <code>==</code></li><li><strong>Use <code>int</code> instead of <code>Integer</code> wherever possible</strong> — primitive types have no caching traps and offer better performance</li><li><strong>Never use Integer objects as lock objects</strong></li></ol><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/64970/</id>
    <link href="https://lichuanyang.top/en/posts/64970/"/>
    <published>2016-12-02T11:45:00.000Z</published>
    <summary>Unveiling the caching mechanism behind the magical number 128 in Java's Integer class, understanding the underlying principles of autoboxing.</summary>
    <title>The Magic of 128 in Java: Understanding Integer Caching</title>
    <updated>2026-06-27T02:18:17.491Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <category term="storm" scheme="https://lichuanyang.top/en/tags/storm/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/en/tags/jstorm/"/>
    <category term="kafka" scheme="https://lichuanyang.top/en/tags/kafka/"/>
    <content>
      <![CDATA[<p>Storm has a very rich surrounding ecosystem. There are ready-made toolkits available for interacting with Kafka, ActiveMQ, HDFS, HBase, and others. Most of these tools, including those introduced today, can also be used normally in JStorm.</p><h2 id="storm-jms"><a href="#storm-jms" class="headerlink" title="storm-jms"></a>storm-jms</h2><p>Implements interaction with JMS implementations such as ActiveMQ.</p><p>Here we mainly introduce JmsSpout. Since sending queue data in Storm is no different from regular Java programs, wrapping it in a dedicated bolt seems unnecessary.</p><p><a href="https://github.com/ptgoetz/storm-jms">https://github.com/ptgoetz/storm-jms</a></p><p>The package includes Spring-based queue configuration loading.</p><h2 id="Usage-Example"><a href="#Usage-Example" class="headerlink" title="Usage Example"></a>Usage Example</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">JmsProvider</span> <span class="variable">jmsQueueProvider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpringJmsProvider</span>(</span><br><span class="line">        <span class="string">&quot;jms-activemq.xml&quot;</span>, <span class="string">&quot;jmsConnectionFactory&quot;</span>,</span><br><span class="line">        <span class="string">&quot;TEST_QUEUE&quot;</span>);</span><br><span class="line"><span class="type">JmsTupleProducer</span> <span class="variable">producer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JsonTupleProducer</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// JMS Queue Spout</span></span><br><span class="line"><span class="type">JmsSpout</span> <span class="variable">queueSpout</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JmsSpout</span>();</span><br><span class="line">queueSpout.setJmsProvider(jmsQueueProvider);</span><br><span class="line">queueSpout.setJmsTupleProducer(producer);</span><br><span class="line">queueSpout.setJmsAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);</span><br><span class="line">queueSpout.setDistributed(<span class="literal">true</span>);</span><br><span class="line"><span class="type">TopologyBuilder</span> <span class="variable">builder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TopologyBuilder</span>();</span><br><span class="line"></span><br><span class="line">builder.setSpout(<span class="string">&quot;jms&quot;</span>, queueSpout, <span class="number">30</span>);</span><br></pre></td></tr></table></figure><h2 id="Configuration-File-Example"><a href="#Configuration-File-Example" class="headerlink" title="Configuration File Example"></a>Configuration File Example</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span>?&gt;</span></span><br><span class="line"><span class="comment">&lt;!--</span></span><br><span class="line"><span class="comment"> Licensed to the Apache Software Foundation (ASF) under one or more</span></span><br><span class="line"><span class="comment"> contributor license agreements.  See the NOTICE file distributed with</span></span><br><span class="line"><span class="comment"> this work for additional information regarding copyright ownership.</span></span><br><span class="line"><span class="comment"> The ASF licenses this file to You under the Apache License, Version 2.0</span></span><br><span class="line"><span class="comment"> (the &quot;License&quot;); you may not use this file except in compliance with</span></span><br><span class="line"><span class="comment"> the License.  You may obtain a copy of the License at</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">     http://www.apache.org/licenses/LICENSE-2.0</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"> Unless required by applicable law or agreed to in writing, software</span></span><br><span class="line"><span class="comment"> distributed under the License is distributed on an &quot;AS IS&quot; BASIS,</span></span><br><span class="line"><span class="comment"> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</span></span><br><span class="line"><span class="comment"> See the License for the specific language governing permissions and</span></span><br><span class="line"><span class="comment"> limitations under the License.</span></span><br><span class="line"><span class="comment">--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> </span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns</span>=<span class="string">&quot;http://www.springframework.org/schema/beans&quot;</span> </span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns:amq</span>=<span class="string">&quot;http://activemq.apache.org/schema/core&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns:xsi</span>=<span class="string">&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xsi:schemaLocation</span>=<span class="string">&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd</span></span></span><br><span class="line"><span class="string"><span class="tag">  http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd&quot;</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">amq:queue</span> <span class="attr">id</span>=<span class="string">&quot;detrUpdateQueue&quot;</span> <span class="attr">physicalName</span>=<span class="string">&quot;TEST_QUEUE&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">amq:connectionFactory</span> <span class="attr">id</span>=<span class="string">&quot;jmsConnectionFactory&quot;</span></span></span><br><span class="line"><span class="tag"><span class="attr">brokerURL</span>=<span class="string">&quot;failover:(tcp://xxx:61616)?jms.prefetchPolicy.queuePrefetch=10&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Usage-Instructions"><a href="#Usage-Instructions" class="headerlink" title="Usage Instructions"></a>Usage Instructions</h2><p>Create a new XML configuration file. The <code>&lt;amq:queue&gt;</code> element configures the queue name.</p><p>The <code>&lt;amq:connectionFactory&gt;</code> element configures the ActiveMQ connection URL.</p><p>Call in code:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">JmsProvider</span> <span class="variable">jmsQueueProvider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpringJmsProvider</span>(</span><br><span class="line">                <span class="string">&quot;jms-activemq.xml&quot;</span>, <span class="string">&quot;jmsConnectionFactory&quot;</span>,</span><br><span class="line">                <span class="string">&quot;TEST_QUEUE&quot;</span>);</span><br></pre></td></tr></table></figure><p>The three parameters are, in order: the configuration file name, the connectionFactory element name, and the queue element name.</p><h2 id="Custom-Output"><a href="#Custom-Output" class="headerlink" title="Custom Output"></a>Custom Output</h2><p>Personalized output can be achieved by implementing JmsTupleProducer.</p><p>Taking JsonTupleProducer as an example:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JsonTupleProducer</span> <span class="keyword">implements</span> <span class="title class_">JmsTupleProducer</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Values <span class="title function_">toTuple</span><span class="params">(Message msg)</span> <span class="keyword">throws</span> JMSException &#123;</span><br><span class="line"><span class="keyword">if</span>(msg <span class="keyword">instanceof</span> TextMessage)&#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> ((TextMessage) msg).getText();</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Values</span>(json);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;</span><br><span class="line">declarer.declare(<span class="keyword">new</span> <span class="title class_">Fields</span>(<span class="string">&quot;json&quot;</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This emits the entire message as a single field.</p><p>declareOutputFields and new Values are the corresponding functions in Storm.</p><p>By processing appropriately in toTuple, customized output can be achieved.</p><h3 id="Disabling-Ack"><a href="#Disabling-Ack" class="headerlink" title="Disabling Ack"></a>Disabling Ack</h3><p>Besides setting the ack count to 0 via <code>conf.setNumAckers(0);</code></p><p>you also need to change the JMS acknowledgment mode to auto-ack:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">queueSpout.setJmsAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);</span><br></pre></td></tr></table></figure><h3 id="Custom-JMS-Connector"><a href="#Custom-JMS-Connector" class="headerlink" title="Custom JMS Connector"></a>Custom JMS Connector</h3><p>The JmsProvider class is used by JmsSpout to establish connections to queues.</p><p>The default SpringJmsProvider obtains JMS connections through Spring configuration.</p><p>If there are special requirements, you can implement the two interfaces in JmsProvider yourself to obtain the ConnectionFactory and queue Destination.</p><h2 id="storm-kafka"><a href="#storm-kafka" class="headerlink" title="storm-kafka"></a>storm-kafka</h2><p><a href="https://github.com/apache/storm/tree/master/external/storm-kafka">https://github.com/apache/storm/tree/master/external/storm-kafka</a></p><p>Retrieving data from Kafka.</p><h2 id="Usage-Example-1"><a href="#Usage-Example-1" class="headerlink" title="Usage Example"></a>Usage Example</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">topic</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>; <span class="comment">//queue name</span></span><br><span class="line"><span class="type">String</span> <span class="variable">zkRoot</span> <span class="operator">=</span> <span class="string">&quot;/kafkastorm&quot;</span>; <span class="comment">//root directory where Kafka records offsets in ZooKeeper, no need to change</span></span><br><span class="line"><span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>; <span class="comment">// consumer id</span></span><br><span class="line"><span class="type">String</span> <span class="variable">kafkaZk</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>; <span class="comment">//Kafka&#x27;s ZooKeeper address</span></span><br><span class="line"><span class="type">BrokerHosts</span> <span class="variable">brokerHosts</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ZkHosts</span>(kafkaZk);</span><br><span class="line"><span class="type">SpoutConfig</span> <span class="variable">kafkaConfig</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpoutConfig</span>(brokerHosts, topic, zkRoot, id);</span><br><span class="line">kafkaConfig.scheme = <span class="keyword">new</span> <span class="title class_">SchemeAsMultiScheme</span>(<span class="keyword">new</span> <span class="title class_">StringScheme</span>());</span><br><span class="line">kafkaConfig.ignoreZkOffsets = <span class="literal">true</span>; <span class="comment">//when false, use offset information stored in Kafka</span></span><br><span class="line">kafkaConfig.startOffsetTime = kafka.api.OffsetRequest.LatestTime(); <span class="comment">//starting position for reading data, only effective when ignoreZkOffsets is set to true</span></span><br><span class="line">kafkaConfig.zkServers = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;()&#123; &#123;</span><br><span class="line">    add(<span class="string">&quot;1&quot;</span>); <span class="comment">//ZooKeeper node storing Kafka offsets</span></span><br><span class="line">&#125; &#125;;</span><br><span class="line">kafkaConfig.zkPort = <span class="number">2181</span>;</span><br><span class="line"><span class="type">TopologyBuilder</span> <span class="variable">builder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TopologyBuilder</span>();</span><br><span class="line">builder.setSpout(topic, <span class="keyword">new</span> <span class="title class_">KafkaSpout</span>(kafkaConfig), <span class="number">10</span>); <span class="comment">//parallelism cannot exceed the number of Kafka partitions</span></span><br><span class="line">builder.setBolt....</span><br></pre></td></tr></table></figure><h2 id="storm-hdfs"><a href="#storm-hdfs" class="headerlink" title="storm-hdfs"></a>storm-hdfs</h2><p>HdfsBolt supports writing data to HDFS.</p><h2 id="Usage-Example-2"><a href="#Usage-Example-2" class="headerlink" title="Usage Example"></a>Usage Example</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">SyncPolicy</span> <span class="variable">syncPolicy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountSyncPolicy</span>(<span class="number">100</span>); <span class="comment">//sync to disk every 100 records</span></span><br><span class="line"><span class="type">DailyRotationPolicy</span> <span class="variable">rotationPolicy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DailyRotationPolicy</span>(); <span class="comment">//archive files daily</span></span><br><span class="line"></span><br><span class="line"><span class="type">FileFolderByDateNameFormat</span> <span class="variable">fileNameFormat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileFolderByDateNameFormat</span>()</span><br><span class="line">        .withPath(<span class="string">&quot;/user/lcy/jdata&quot;</span>); <span class="comment">//HDFS path</span></span><br><span class="line"><span class="type">RecordFormat</span> <span class="variable">format</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DelimitedRecordFormat</span>()</span><br><span class="line">        .withFieldDelimiter(<span class="string">&quot;,&quot;</span>); <span class="comment">//delimiter between fields</span></span><br><span class="line"></span><br><span class="line"><span class="type">UmeHdfsBolt</span> <span class="variable">hdfsbolt</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UmeHdfsBolt</span>()</span><br><span class="line">        .withFsUrl(<span class="string">&quot;hdfs://:8020&quot;</span>) <span class="comment">//HDFS URL</span></span><br><span class="line">        .withFileNameFormat(fileNameFormat)</span><br><span class="line">        .withRecordFormat(format)</span><br><span class="line">        .withRotationPolicy(rotationPolicy)</span><br><span class="line">        .withSyncPolicy(syncPolicy);</span><br></pre></td></tr></table></figure><h2 id="Example-Description"><a href="#Example-Description" class="headerlink" title="Example Description"></a>Example Description</h2><p>When coded according to the example, the output to HDFS is as follows:</p><p>Each record received by the bolt becomes one line in HDFS;</p><p>The output path to HDFS is: configured path &#x2F; date &#x2F; default filename;</p><p>Each thread of hdfsbolt outputs to a separate file; topology restarts create new files;</p><p>Files are archived daily.</p><h2 id="Additional-Features"><a href="#Additional-Features" class="headerlink" title="Additional Features"></a>Additional Features</h2><p>FileSizeRotationPolicy supports archiving by fixed file size; TimedRotationPolicy supports archiving by fixed time;</p><p>If other archiving methods are needed, you can implement the FileRotationPolicy interface, referring to DailyRotationPolicy source code.</p><p>Custom file paths and filenames can be implemented through the FileNameFormat interface, referring to FileFolderByDateNameFormat.</p><h2 id="FileFolderByDateNameFormat"><a href="#FileFolderByDateNameFormat" class="headerlink" title="FileFolderByDateNameFormat"></a>FileFolderByDateNameFormat</h2><p>FileFolderByDateNameFormat implements the commonly used storage path pattern.<br>Usage example:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">FileFolderByDateNameFormat</span> <span class="variable">fileNameFormat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileFolderByDateNameFormat</span>()</span><br><span class="line">         .withPath(<span class="string">&quot;/user/test&quot;</span>).withName(<span class="string">&quot;&quot;</span>);</span><br></pre></td></tr></table></figure><p>This will automatically append a date after the path, and dates will also be included in the filename.</p><h2 id="Source-Reference"><a href="#Source-Reference" class="headerlink" title="Source Reference"></a>Source Reference</h2><p>In addition to the storm-hdfs source code itself, you can refer to <a href="https://github.com/lcy362/StormTrooper">https://github.com/lcy362/StormTrooper</a> for the implementations of FileFolderByDateNameFormat and DailyRotationPolicy.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/22103/">https://lichuanyang.top/en/posts/22103/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/22103/</id>
    <link href="https://lichuanyang.top/en/posts/22103/"/>
    <published>2016-11-16T12:16:00.000Z</published>
    <summary>A panoramic view of Storm/JStorm ecosystem tools, introducing how to connect ActiveMQ, Kafka, HDFS and other external systems to build complete data pipelines.</summary>
    <title>Storm/JStorm Ecosystem and Peripheral Tools: Connecting Storm to ActiveMQ, Kafka, HDFS and More</title>
    <updated>2026-06-27T03:48:11.127Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="storm" scheme="https://lichuanyang.top/en/tags/storm/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/en/tags/jstorm/"/>
    <content>
      <![CDATA[<h2 id="UI-Overview"><a href="#UI-Overview" class="headerlink" title="UI Overview"></a>UI Overview</h2><p>Compared to Storm, JStorm’s UI provides more detailed monitoring items. The UI itself is a WAR package running in Tomcat, making secondary development relatively easy.</p><h2 id="Cluster-Page"><a href="#Cluster-Page" class="headerlink" title="Cluster Page"></a>Cluster Page</h2><h3 id="Cluster-Summary-Cluster-Stats-Topology-Summary"><a href="#Cluster-Summary-Cluster-Stats-Topology-Summary" class="headerlink" title="Cluster Summary, Cluster Stats, Topology Summary"></a>Cluster Summary, Cluster Stats, Topology Summary</h3><p>Overall cluster information. The conf section contains the configuration of the nimbus node.</p><h3 id="Topology-Summary"><a href="#Topology-Summary" class="headerlink" title="Topology Summary"></a>Topology Summary</h3><p>A list of all currently running topologies along with summary information. The conf section corresponds to topology-specific configuration items.</p><h3 id="Supervisor-Summary"><a href="#Supervisor-Summary" class="headerlink" title="Supervisor Summary"></a>Supervisor Summary</h3><p>A list of all supervisors, where you can view supervisor configuration, logs, etc.</p><h2 id="Topology-Page"><a href="#Topology-Page" class="headerlink" title="Topology Page"></a>Topology Page</h2><h3 id="Topology-Summary-1"><a href="#Topology-Summary-1" class="headerlink" title="Topology Summary"></a>Topology Summary</h3><p>The main focus is on the Topology Graph section, which is a summary of the Component Metrics page and provides a relatively intuitive view of the running status.</p><p>Among these, node size and arrow thickness represent data volume, while node color is used to distinguish between spouts and bolts. Arrow color corresponds to the TupleLifeCycle attribute, going from green (fast) to red (slow). When arrows turn red, you can consider increasing the parallelism of downstream nodes.</p><h3 id="Component-Metrics"><a href="#Component-Metrics" class="headerlink" title="Component Metrics"></a>Component Metrics</h3><p>Contains statistical information for each spout and bolt.</p><p>Parameter descriptions:</p><p>tasks: Parallelism. Corresponds to the parallelism set for each spout&#x2F;bolt in the code.</p><p>emitted, acked, failed: The number of messages emitted &#x2F; received ack &#x2F; not received ack.</p><p>sendTps, recvTps: Send &#x2F; receive data TPS.</p><p>Note that the above interaction metrics include interactions with Storm’s own components (topology_master, acker), so they can only be used to check running status and are <strong>not suitable for statistical purposes</strong>.</p><p>process: For bolts, this is the execution time of the execute() function. For spouts, this is the time for the entire message (including all downstream steps) to be fully processed. This is the main parameter reflecting system operating efficiency.</p><p>TupleLifeCycle: The time from when an upstream message is emitted to when it is fully processed. When this differs significantly from process, it indicates the message spent a long time waiting in the bolt queue, and you may consider increasing concurrency.</p><p>deser, ser: Serialization &#x2F; deserialization time, generally does not require attention. In Storm, every emitted piece of data requires serialization and deserialization.</p><p>error: Hover over E to view specific error messages. <strong>Mainly look for entries with exception stack traces</strong>; messages like “is dead” or “no response” do not require attention.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/31996/">https://lichuanyang.top/en/posts/31996/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/31996/</id>
    <link href="https://lichuanyang.top/en/posts/31996/"/>
    <published>2016-11-16T11:58:00.000Z</published>
    <summary>Detailed introduction to JStorm UI features, including Cluster Summary, Topology Summary and other monitoring items, plus a secondary development guide.</summary>
    <title>JStorm UI Introduction</title>
    <updated>2026-06-08T07:50:49.096Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="storm" scheme="https://lichuanyang.top/en/tags/storm/"/>
    <content>
      <![CDATA[<p>Storm (and Alibaba’s open-source fork JStorm) is a distributed real-time computation framework. Its programming model has some key differences from traditional Java programs. If you don’t understand these differences, it’s easy to write code that runs fine locally but produces all sorts of bizarre bugs when deployed to a cluster.</p><p>This article doesn’t dive deep into Storm’s internals. Instead, it focuses on the few critical differences that developers most need to know — helping you get up to speed quickly and avoid common pitfalls.</p><span id="more"></span><h2 id="A-Quick-Look-at-Storm’s-Runtime"><a href="#A-Quick-Look-at-Storm’s-Runtime" class="headerlink" title="A Quick Look at Storm’s Runtime"></a>A Quick Look at Storm’s Runtime</h2><p>A Storm Topology consists of several Spouts (data sources) and Bolts (processing logic), distributed across multiple Workers (processes). A single Worker may simultaneously run multiple threads belonging to multiple Spouts&#x2F;Bolts.</p><p>The key thing to understand is: <strong>your code gets serialized, distributed to each Worker, and executed there — not on the machine that submits the Topology.</strong></p><h2 id="Three-Key-Differences-from-Regular-Java-Programs"><a href="#Three-Key-Differences-from-Regular-Java-Programs" class="headerlink" title="Three Key Differences from Regular Java Programs"></a>Three Key Differences from Regular Java Programs</h2><h3 id="1-The-main-Method-Only-Runs-on-Nimbus"><a href="#1-The-main-Method-Only-Runs-on-Nimbus" class="headerlink" title="1. The main Method Only Runs on Nimbus"></a>1. The main Method Only Runs on Nimbus</h3><p>The <code>main</code> method only executes on the Nimbus (Storm’s master node) when submitting the Topology. Its sole purpose is to build the Topology structure and submit it — <strong>the actual Spout and Bolt code does not run in the same process as main</strong>.</p><p>What this means:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ This won&#x27;t work as you expect!</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="comment">// Spring context initialized here, but Worker processes can&#x27;t see it</span></span><br><span class="line">    <span class="type">ApplicationContext</span> <span class="variable">ctx</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassPathXmlApplicationContext</span>(<span class="string">&quot;applicationContext.xml&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="type">TopologyBuilder</span> <span class="variable">builder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TopologyBuilder</span>();</span><br><span class="line">    builder.setSpout(<span class="string">&quot;myspout&quot;</span>, <span class="keyword">new</span> <span class="title class_">MySpout</span>());</span><br><span class="line">    builder.setBolt(<span class="string">&quot;mybolt&quot;</span>, <span class="keyword">new</span> <span class="title class_">MyBolt</span>()).shuffleGrouping(<span class="string">&quot;myspout&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    StormSubmitter.submitTopology(<span class="string">&quot;mytopo&quot;</span>, config, builder.createTopology());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Spring configurations, database connection pools, encryption keys, etc., must not be initialized in <code>main</code>. Only Storm’s own configuration items (<code>Config</code> object) and Topology structure building belong there.</p><h3 id="2-Spout-Bolt-Lifecycle"><a href="#2-Spout-Bolt-Lifecycle" class="headerlink" title="2. Spout&#x2F;Bolt Lifecycle"></a>2. Spout&#x2F;Bolt Lifecycle</h3><p>Each Spout and Bolt has its own lifecycle methods:</p><table><thead><tr><th>Component</th><th>Initialization</th><th>Per-Message</th><th>Shutdown</th></tr></thead><tbody><tr><td><strong>Spout</strong></td><td><code>open()</code></td><td><code>nextTuple()</code></td><td><code>close()</code></td></tr><tr><td><strong>Bolt</strong></td><td><code>prepare()</code></td><td><code>execute()</code></td><td><code>cleanup()</code></td></tr></tbody></table><p>Key points:</p><ul><li><strong><code>prepare()</code>&#x2F;<code>open()</code> runs once when the Worker process starts</strong> — this is the correct place for initialization. Spring containers, database connections, configuration loading — put them all here.</li><li><strong>When multiple Bolts need to load Spring, use the same configuration</strong>: Multiple Bolts in the same Worker may share a JVM process. Repeatedly initializing Spring containers can waste resources or even cause conflicts.</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyBolt</span> <span class="keyword">extends</span> <span class="title class_">BaseRichBolt</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> SomeService service;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">prepare</span><span class="params">(Map stormConf, TopologyContext context, OutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="comment">// ✅ Initialization goes here</span></span><br><span class="line">        <span class="type">ApplicationContext</span> <span class="variable">ctx</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassPathXmlApplicationContext</span>(<span class="string">&quot;applicationContext.xml&quot;</span>);</span><br><span class="line">        <span class="built_in">this</span>.service = ctx.getBean(SomeService.class);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple input)</span> &#123;</span><br><span class="line">        <span class="comment">// Process each message</span></span><br><span class="line">        service.process(input);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-Serialization-—-The-Most-Common-Pitfall"><a href="#3-Serialization-—-The-Most-Common-Pitfall" class="headerlink" title="3. Serialization — The Most Common Pitfall"></a>3. Serialization — The Most Common Pitfall</h3><p>Since Spouts and Bolts may run on different Workers (even different machines), <strong>all data transmitted through Storm must be serializable</strong>:</p><ul><li>Tuples emitted via <code>emit()</code></li><li>Spout&#x2F;Bolt member variables (if initialized in constructors or static blocks rather than in <code>prepare()</code>)</li><li>Objects passed through configuration</li></ul><p>Storm defaults to <strong>Kryo</strong> serialization, which is faster than Java’s native serialization, but has some requirements:</p><ul><li><strong>Classes must have a public no-arg constructor</strong>. If a class lacks one (as with some third-party library classes), you’ll encounter <code>IllegalArgumentException</code></li><li><strong>Inner classes and anonymous classes are not supported by default</strong> and need manual registration</li></ul><p>If adding a no-arg constructor isn’t feasible, you can fall back:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">config.setFallBackOnJavaSerialization(<span class="literal">true</span>);  <span class="comment">// Fall back to Java serialization</span></span><br></pre></td></tr></table></figure><h2 id="A-Complete-WordCount-Example"><a href="#A-Complete-WordCount-Example" class="headerlink" title="A Complete WordCount Example"></a>A Complete WordCount Example</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Spout: continuously emits sentences</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SentenceSpout</span> <span class="keyword">extends</span> <span class="title class_">BaseRichSpout</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> SpoutOutputCollector collector;</span><br><span class="line">    <span class="keyword">private</span> String[] sentences = &#123;</span><br><span class="line">        <span class="string">&quot;hello world&quot;</span>, <span class="string">&quot;hello storm&quot;</span>, <span class="string">&quot;goodbye world&quot;</span></span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">index</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">open</span><span class="params">(Map conf, TopologyContext context, SpoutOutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.collector = collector;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">nextTuple</span><span class="params">()</span> &#123;</span><br><span class="line">        collector.emit(<span class="keyword">new</span> <span class="title class_">Values</span>(sentences[index]));</span><br><span class="line">        index = (index + <span class="number">1</span>) % sentences.length;</span><br><span class="line">        Utils.sleep(<span class="number">1000</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;</span><br><span class="line">        declarer.declare(<span class="keyword">new</span> <span class="title class_">Fields</span>(<span class="string">&quot;sentence&quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Bolt 1: Split words</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SplitBolt</span> <span class="keyword">extends</span> <span class="title class_">BaseRichBolt</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> OutputCollector collector;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">prepare</span><span class="params">(Map conf, TopologyContext context, OutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.collector = collector;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple tuple)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">sentence</span> <span class="operator">=</span> tuple.getStringByField(<span class="string">&quot;sentence&quot;</span>);</span><br><span class="line">        <span class="keyword">for</span> (String word : sentence.split(<span class="string">&quot; &quot;</span>)) &#123;</span><br><span class="line">            collector.emit(<span class="keyword">new</span> <span class="title class_">Values</span>(word));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;</span><br><span class="line">        declarer.declare(<span class="keyword">new</span> <span class="title class_">Fields</span>(<span class="string">&quot;word&quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Bolt 2: Count words</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CountBolt</span> <span class="keyword">extends</span> <span class="title class_">BaseRichBolt</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> OutputCollector collector;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, Integer&gt; counts = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">prepare</span><span class="params">(Map conf, TopologyContext context, OutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.collector = collector;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple tuple)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">word</span> <span class="operator">=</span> tuple.getStringByField(<span class="string">&quot;word&quot;</span>);</span><br><span class="line">        counts.merge(word, <span class="number">1</span>, Integer::sum);</span><br><span class="line">        System.out.println(word + <span class="string">&quot;: &quot;</span> + counts.get(word));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="A-2026-Perspective"><a href="#A-2026-Perspective" class="headerlink" title="A 2026 Perspective"></a>A 2026 Perspective</h2><p>Storm was once the de facto standard for real-time computation, but in recent years <strong>Apache Flink</strong> has become the mainstream choice for stream processing. Flink offers better exactly-once semantics, richer window operations, and more mature state management. If you’re choosing a framework for a new project, I would recommend considering Flink first.</p><p>That said, if you’re maintaining an existing Storm&#x2F;JStorm legacy system, the content in this article can still help you quickly identify issues. Once you understand Storm’s Worker model and serialization mechanism, many “mysterious production bugs” actually have traceable causes.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/27021/">https://lichuanyang.top/en/posts/27021/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/27021/</id>
    <link href="https://lichuanyang.top/en/posts/27021/"/>
    <published>2016-11-16T10:21:00.000Z</published>
    <summary>A quick start guide to Storm/JStorm programming, highlighting the key differences in initialization between topology runtime and traditional Java programs.</summary>
    <title>Five Minutes to Learn Storm Coding: JStorm/Storm Coding Principles and Differences from Regular Java Programs</title>
    <updated>2026-06-27T03:48:18.980Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <category term="monitoring" scheme="https://lichuanyang.top/en/tags/monitoring/"/>
    <content>
      <![CDATA[<h2 id="Security-Risks-of-Web-Console"><a href="#Security-Risks-of-Web-Console" class="headerlink" title="Security Risks of Web Console"></a>Security Risks of Web Console</h2><p>ActiveMQ’s web console is built on Jetty, and its permission management is also based on Jetty. Based on requirements, different permissions can be assigned to different users. Jetty’s permission management is fairly flexible, though it can be a bit cumbersome to configure. You can specify whether a particular role (role) has access to a specific page.</p><h2 id="JAAS-Authentication-Configuration"><a href="#JAAS-Authentication-Configuration" class="headerlink" title="JAAS Authentication Configuration"></a>JAAS Authentication Configuration</h2><p>Below is a brief introduction to the configuration method. You only need to modify the following files under <code>/conf</code>: <code>jetty.xml</code> and <code>jetty-realm.properties</code>.</p><p><strong>1. jetty-realm.properties</strong></p><p>This file configures all users’ usernames, passwords, and their associated roles, following this format:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">username: password [,rolename ...]</span><br></pre></td></tr></table></figure><h2 id="Role-and-Permission-Definition"><a href="#Role-and-Permission-Definition" class="headerlink" title="Role and Permission Definition"></a>Role and Permission Definition</h2><p><strong>2. jetty.xml</strong></p><p>First, configure a Constraint class for each role, where the <code>roles</code> correspond to the role names in <code>jetty-realm.properties</code>:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;securityConstraint&quot;</span> <span class="attr">class</span>=<span class="string">&quot;org.eclipse.jetty.util.security.Constraint&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;name&quot;</span> <span class="attr">value</span>=<span class="string">&quot;BASIC&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;roles&quot;</span> <span class="attr">value</span>=<span class="string">&quot;admin&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;authenticate&quot;</span> <span class="attr">value</span>=<span class="string">&quot;true&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Then configure the <code>securityConstraintMapping</code>:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;securityConstraintMapping&quot;</span> <span class="attr">class</span>=<span class="string">&quot;org.eclipse.jetty.security.ConstraintMapping&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;constraint&quot;</span> <span class="attr">ref</span>=<span class="string">&quot;securityConstraint&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;pathSpec&quot;</span> <span class="attr">value</span>=<span class="string">&quot;/admin/send.jsp/&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><p>This means the role associated with <code>securityConstraint</code> can access the <code>/admin/send.jsp</code> page.</p><p>You can use <code>/*</code> to represent all pages that are <strong>not individually configured</strong>.</p><p>For example, suppose we need to create a read-only user. We can configure two roles: <code>admin</code> and <code>readonly</code>. Both roles need a <code>/*</code> ConstraintMapping entry, and then the <code>admin</code> role gets additional entries for all write-operation pages, including <code>/admin/deleteDestination.action/*</code>, <code>/admin/purgeDestination.action/*</code>, etc.</p><p>Finally, list all ConstraintMappings in the <code>constraintMappings</code> property of the <code>ConstraintSecurityHandler</code>:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;securityHandler&quot;</span> <span class="attr">class</span>=<span class="string">&quot;org.eclipse.jetty.security.ConstraintSecurityHandler&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;constraintMappings&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">list</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">ref</span> <span class="attr">bean</span>=<span class="string">&quot;securityConstraintMapping&quot;</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">list</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><p>This completes the permission configuration for ActiveMQ Web Console users.</p><hr><p>Source: <a href="https://lichuanyang.top/en/posts/32479/">https://lichuanyang.top/en/posts/32479/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/32479/</id>
    <link href="https://lichuanyang.top/en/posts/32479/"/>
    <published>2016-01-05T10:09:00.000Z</published>
    <summary>Introduction to ActiveMQ Web Console permission management configuration based on Jetty, implementing access control for different user roles.</summary>
    <title>ActiveMQ Web Console Security Configuration</title>
    <updated>2026-06-27T03:50:02.434Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="storm" scheme="https://lichuanyang.top/en/tags/storm/"/>
    <content>
      <![CDATA[<h2 id="Storm-UI-Overview"><a href="#Storm-UI-Overview" class="headerlink" title="Storm UI Overview"></a>Storm UI Overview</h2><p>Storm UI is very helpful for troubleshooting issues encountered during Storm usage, but some properties have unclear meanings. Although they are all simple concepts, not knowing them can be quite frustrating.</p><p>One thing to note: when you hover over the title bar in the UI, you can see the specific details of that property. Several highly-ranked Google articles are essentially just organizing this information.</p><p>Most properties are straightforward — you know what they mean just by seeing the name. Here I’ll only list some properties that might cause confusion, to help everyone troubleshoot issues more easily.</p><h2 id="Key-Metrics-Explained"><a href="#Key-Metrics-Explained" class="headerlink" title="Key Metrics Explained"></a>Key Metrics Explained</h2><p><strong>emitted and transferred</strong>: “emitted” refers to the number of data items emitted, which is the number of times the <code>emit</code> method of <code>OutputCollector</code> is called. “transferred” is the actual number of tuples sent to the next task. At first glance they seem the same, right? In most cases they are indeed the same. However, for example, if a bolt emits data but no downstream bolt consumes it, the bolt’s transfer count will be 0. Another example: if bolt A uses the <code>all</code> group strategy (every bolt must receive it) to emit tuples to bolt B, then the transferred count will be a multiple of the emitted count.</p><p><strong>execute latency and process latency</strong>: “execute latency” is straightforward — it’s the execution time of the <code>execute()</code> method in code. “process latency” is the time from when <code>execute</code> starts until the <code>ack</code> method is called. It can be considered as the time required for business code execution. Under normal circumstances, execute latency is greater than process latency, but if you never call <code>ack</code>, process latency will be much greater than execute latency.</p><p><strong>Spout’s complete latency</strong>: You can refer to Storm’s <a href="http://xumingming.sinaapp.com/127/twitter-storm%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%B6%88%E6%81%AF%E4%B8%8D%E4%B8%A2%E5%A4%B1/">ack mechanism</a>. This is the time from when a tuple is emitted until that tuple is acked. More precisely, it’s the time difference between when the spout calls the emit method and when the ack method is called — in other words, it’s the time required for the tuple tree generated by that tuple to be fully processed.</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/62259/</id>
    <link href="https://lichuanyang.top/en/posts/62259/"/>
    <published>2015-11-06T13:44:00.000Z</published>
    <summary>An interpretation of key monitoring metrics in Storm UI, helping operations staff quickly identify topology performance bottlenecks.</summary>
    <title>Understanding Key Properties in Storm UI</title>
    <updated>2026-06-27T03:53:05.202Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="activemq" scheme="https://lichuanyang.top/en/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="Problem-Symptoms"><a href="#Problem-Symptoms" class="headerlink" title="Problem Symptoms"></a>Problem Symptoms</h2><p>Recently, while using ActiveMQ’s connection pool, I discovered a very serious memory leak issue.</p><h2 id="Investigation-Process"><a href="#Investigation-Process" class="headerlink" title="Investigation Process"></a>Investigation Process</h2><p>Through jmap monitoring, it can be seen that <code>java.util.concurrent.locks.ReentrantLock</code> and <code>org.apache.activemq.pool.PooledConnection</code> occupy a very large amount of memory, and the growth rate is also very fast.</p><h2 id="Root-Cause-Analysis"><a href="#Root-Cause-Analysis" class="headerlink" title="Root Cause Analysis"></a>Root Cause Analysis</h2><p>After searching online, I found exactly the corresponding ActiveMQ <a href="https://issues.apache.org/jira/browse/AMQ-3997">bug report</a>: <a href="https://issues.apache.org/jira/browse/AMQ-3997">https://issues.apache.org/jira/browse/AMQ-3997</a></p><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>This bug has been fixed in version 5.7, so it can be resolved by upgrading the version.</p><p>At the same time, there is another solution, which is to use Spring’s connection pool to replace ActiveMQ’s built-in connection pool. The configuration is as follows:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">  <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;jmsConnectionFactory&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;org.apache.activemq.ActiveMQConnectionFactory&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;brokerURL&quot;</span> <span class="attr">value</span>=<span class="string">&quot;vm://205-amq-broker2?create=false<span class="symbol">&amp;amp;</span>waitForStart=10000&quot;</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!--        &lt;bean id=&quot;pooledConnectionFactory&quot;</span></span><br><span class="line"><span class="comment">                class=&quot;org.apache.activemq.pool.PooledConnectionFactory&quot; init-method=&quot;start&quot; destroy-method=&quot;stop&quot;&gt;</span></span><br><span class="line"><span class="comment">                &lt;property name=&quot;maxConnections&quot; value=&quot;8&quot; /&gt;</span></span><br><span class="line"><span class="comment">                &lt;property name=&quot;connectionFactory&quot; ref=&quot;jmsConnectionFactory&quot; /&gt;</span></span><br><span class="line"><span class="comment">        &lt;/bean&gt;--&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;cachedConnectionFactory&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;org.springframework.jms.connection.CachingConnectionFactory&quot;</span>&gt;</span></span><br><span class="line">                        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;targetConnectionFactory&quot;</span> <span class="attr">ref</span>=<span class="string">&quot;jmsConnectionFactory&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">                        <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;sessionCacheSize&quot;</span> <span class="attr">value</span>=<span class="string">&quot;10&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;jmsConfig&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;org.apache.camel.component.jms.JmsConfiguration&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;connectionFactory&quot;</span> <span class="attr">ref</span>=<span class="string">&quot;cachedConnectionFactory&quot;</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;concurrentConsumers&quot;</span> <span class="attr">value</span>=<span class="string">&quot;10&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">&quot;activemq&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;org.apache.activemq.camel.component.ActiveMQComponent&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;configuration&quot;</span> <span class="attr">ref</span>=<span class="string">&quot;jmsConfig&quot;</span>/&gt;</span></span><br></pre></td></tr></table></figure><hr><p>Source: <a href="https://lichuanyang.top/en/posts/13925/">https://lichuanyang.top/en/posts/13925/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/13925/</id>
    <link href="https://lichuanyang.top/en/posts/13925/"/>
    <published>2015-08-08T13:16:00.000Z</published>
    <summary>Documenting a severe memory leak issue in ActiveMQ 5.6 connection pool, using jmap monitoring to locate abnormal growth of ReentrantLock and PooledConnection.</summary>
    <title>ActiveMQ 5.6 Connection Pool Memory Leak Issue</title>
    <updated>2026-06-27T03:51:55.450Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Message Queue" scheme="https://lichuanyang.top/en/categories/Message-Queue/"/>
    <category term="camel" scheme="https://lichuanyang.top/en/tags/camel/"/>
    <category term="java" scheme="https://lichuanyang.top/en/tags/java/"/>
    <content>
      <![CDATA[<h2 id="Test-Scenario"><a href="#Test-Scenario" class="headerlink" title="Test Scenario"></a>Test Scenario</h2><p><span style="font-family: Arial,Helvetica,sans-serif; background-color: #ffffff;">The company uses ActiveMQ and Camel for message distribution. Previously, the data volume wasn’t very large, so we never really considered efficiency issues, and the research into Camel’s working principles wasn’t deep. However, recently, as the business volume increased, Camel’s efficiency has gradually become a bottleneck, so I got a general understanding of Camel’s working principles based on logs. Although Camel is embedded into ActiveMQ, during the working process, Camel and ActiveMQ are actually relatively independent. We configure a connection to ActiveMQ in Camel.</span></p><pre name="code" class="html">http://camel.apache.org/activemq.html</pre><p>Regarding the VM transport method, refer to <a href="http://activemq.apache.org/vm-transport-reference.html">http://activemq.apache.org/vm-transport-reference.html</a></p><h2 id="Bottleneck-Analysis"><a href="#Bottleneck-Analysis" class="headerlink" title="Bottleneck Analysis"></a>Bottleneck Analysis</h2><p>After checking the logs, I found that with this configuration, Camel has a very serious problem: every time Camel performs a forwarding operation, it creates a new connection to ActiveMQ and then closes it. This severely slows down the forwarding efficiency, since in fact, the same connection could be reused for every forwarding operation.</p><h2 id="Optimization-Suggestions"><a href="#Optimization-Suggestions" class="headerlink" title="Optimization Suggestions"></a>Optimization Suggestions</h2><p>So I looked up the Camel documentation and found <a href="http://camel.apache.org/activemq.html">http://camel.apache.org/activemq.html</a>. It contains configuration for thread pools:</p><p>&nbsp;</p><pre name="code" class="html">&lt;pre name="code" class="html"&gt;&lt;bean id="jmsConnectionFactory"    class="org.apache.activemq.ActiveMQConnectionFactory"&gt;   &lt;property name="brokerURL" value="tcp://localhost:61616" /&gt;&lt;/bean&gt;&lt;bean id="pooledConnectionFactory"    class="org.apache.activemq.pool.PooledConnectionFactory" init-method="start" destroy-method="stop"&gt;   &lt;property name="maxConnections" value="8" /&gt;   &lt;property name="connectionFactory" ref="jmsConnectionFactory" /&gt;&lt;/bean&gt;&lt;bean id="jmsConfig"    class="org.apache.camel.component.jms.JmsConfiguration"&gt;   &lt;property name="connectionFactory" ref="pooledConnectionFactory"/&gt;   &lt;property name="concurrentConsumers" value="10"/&gt;&lt;/bean&gt;&lt;bean id="activemq"     class="org.apache.activemq.camel.component.ActiveMQComponent"&gt;    &lt;property name="configuration" ref="jmsConfig"/&gt;    &lt;!-- if we are using transacted then enable CACHE_CONSUMER (if not using XA) to run faster         see more details at: http://camel.apache.org/jms    &lt;property name="transacted" value="true"/&gt;    &lt;property name="cacheLevelName" value="CACHE_CONSUMER" /&gt;    --&gt;&lt;/bean&gt;</pre><p>This fits our needs exactly. And by switching the connection to multi-threaded, we can further improve efficiency.</p><p>&nbsp;</p><p>It’s worth noting that if you’re using ActiveMQ 5.6, doing this will cause a memory leak. I will elaborate on this in the next blog post.</p><h2 id="nbsp"><a href="#nbsp" class="headerlink" title="&nbsp;"></a>&nbsp;</h2><p>Source: <a href="https://lichuanyang.top/en/posts/17762/">https://lichuanyang.top/en/posts/17762/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/17762/</id>
    <link href="https://lichuanyang.top/en/posts/17762/"/>
    <published>2015-08-08T12:55:00.000Z</published>
    <summary>Analyzes the working principle and efficiency bottlenecks of Apache Camel message forwarding, providing insights for performance optimization in high-data-volume scenarios.</summary>
    <title>About Apache Camel's Message Forwarding Efficiency</title>
    <updated>2026-06-27T03:51:55.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Tech Miscellany" scheme="https://lichuanyang.top/en/categories/Tech-Miscellany/"/>
    <category term="big-data" scheme="https://lichuanyang.top/en/tags/big-data/"/>
    <category term="lda" scheme="https://lichuanyang.top/en/tags/lda/"/>
    <content>
      <![CDATA[<p>Reposted from <a href="http://leyew.blog.51cto.com/5043877/860255#559183-tsina-1-46862-ed0973a0c870156ed15f06a6573c8bf0">http://leyew.blog.51cto.com/5043877/860255#559183-tsina-1-46862-ed0973a0c870156ed15f06a6573c8bf0</a></p><p>A few days ago I started learning LDA, took many detours, and was still completely confused about LDA. After reading this document, I finally understood what LDA is for.</p><p>&nbsp;</p><h3 id="LDA-Latent-Dirichlet-Allocation-Learning-Notes"><a href="#LDA-Latent-Dirichlet-Allocation-Learning-Notes" class="headerlink" title="LDA (Latent Dirichlet Allocation) Learning Notes"></a>LDA (Latent Dirichlet Allocation) Learning Notes</h3><p>I’ve been studying the LDA algorithm recently, and after a few days of struggle, I’ve finally gained a rough understanding of the overall framework and process of this algorithm.</p><h4 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h4><p>What LDA does in simple terms is cluster a bunch of documents (so it’s unsupervised learning), where each topic represents a category, and the number of topics to cluster into is specified in advance. The clustering result is a probability, not a boolean 100% belonging to a certain category. A foreign blog [1] has a clear example, directly cited:</p><blockquote><p>Suppose you have the following set of sentences:</p><ul><li>I like to eat broccoli and bananas.</li><li>I ate a banana and spinach smoothie for breakfast.</li><li>Chinchillas and kittens are cute.</li><li>My sister adopted a kitten yesterday.</li><li>Look at this cute hamster munching on a piece of broccoli.</li></ul><p>What is latent Dirichlet allocation? It’s a way of automatically discovering topics that these sentences contain. For example, given these sentences and asked for 2 topics, LDA might produce something like</p><ul><li><strong>Sentences 1 and 2</strong>: 100% Topic A</li><li><strong>Sentences 3 and 4</strong>: 100% Topic B</li><li><strong>Sentence 5</strong>: 60% Topic A, 40% Topic B</li><li><strong>Topic A</strong>: 30% broccoli, 15% bananas, 10% breakfast, 10% munching, … (at which point, you could interpret topic A to be about food)</li><li><strong>Topic B</strong>: 20% chinchillas, 20% kittens, 20% cute, 15% hamster, … (at which point, you could interpret topic B to be about cute animals)</li></ul></blockquote><p>Looking at the result for sentence 5, we can see it’s clearly a probabilistic clustering result (sentences 1 and 2 happen to be 100% deterministic results).</p><p>Looking at the results in the example, besides obtaining a probabilistic clustering result for each sentence, each topic has representative words with proportions. Taking Topic A as an example, it means that among all words corresponding to Topic A, 30% of them are “broccoli”. In the LDA algorithm, every word in every document is mapped to a Topic, so this proportion can be calculated. These words serve as good guidance for describing this Topic, which I think is the advantage of LDA over traditional text clustering.</p><h4 id="LDA-Overall-Process"><a href="#LDA-Overall-Process" class="headerlink" title="LDA Overall Process"></a>LDA Overall Process</h4><p>First, let’s define the meaning of some letters:</p><ul><li>Document collection <strong>D</strong>, topic collection <strong>T</strong></li><li>Each document d in D is viewed as a word sequence <strong>&lt; w1, w2, …, wn &gt;</strong>, where wi represents the i-th word, and d has n words. (In LDA this is called a <em>word bag</em>, where the actual position of each word has no effect on the LDA algorithm)</li><li>All different words involved in D form a large collection <strong>VOCABULARY</strong> (abbreviated as <strong>VOC</strong>)</li></ul><p>LDA takes document collection D as input (with common preprocessing steps like tokenization, stop word removal, and stemming omitted), and hopes to train two result vectors (<strong>assuming clustering into k Topics, with m words in VOC</strong>):</p><ul><li><strong>For each document d in D, the probability of mapping to different topics θ<sub>d</sub> &lt; p<sub>t1</sub>,…, p<sub>tk</sub> &gt;</strong>, where p<sub>ti</sub> represents the probability that d maps to the i-th topic in T. The calculation method is intuitive: p<sub>ti</sub> &#x3D; n<sub>ti</sub>&#x2F;n, where n<sub>ti</sub> represents the number of words in d that correspond to the i-th topic, and n is the total number of words in d.</li><li><strong>For each topic t in T, the probability of generating different words φ<sub>t</sub> &lt; p<sub>w1</sub>,…, p<sub>wm</sub> &gt;</strong>, where p<sub>wi</sub> represents the probability of t generating the i-th word in VOC. The calculation method is equally intuitive: p<sub>wi</sub> &#x3D; N<sub>wi</sub>&#x2F;N, where N<sub>wi</sub> represents the number of times the i-th word in VOC corresponds to topic t, and N represents the total number of words that correspond to topic t.</li></ul><p>The core formula of LDA is as follows:</p><p>*<em>p(w|d) &#x3D; p(w|t)<em>p(t|d)</em></em></p><p>Intuitively, this formula uses Topic as an intermediate layer, and through the current θ<sub>d</sub> and φ<sub>t</sub>, gives the probability of word w appearing in document d. Here, p(t|d) is calculated using θ<sub>d</sub>, and p(w|t) is calculated using φ<sub>t</sub>.</p><p>In practice, using the current θ<sub>d</sub> and φ<sub>t</sub>, we can calculate p(w|d) for a word in a document when it maps to any Topic, and then based on these results, update which topic this word should correspond to. Then, if this update changes the topic the word corresponds to, it will in turn affect θ<sub>d</sub> and φ<sub>t</sub>.</p><p>When the LDA algorithm starts, it first randomly assigns values to θ<sub>d</sub> and φ<sub>t</sub> (for all d and t). Then the above process is repeated continuously, and the final converged result is the output of LDA.</p><p>Let me elaborate more on this iterative learning process:</p><p>For the i-th word w<sub>i</sub> in a specific document d<sub>s</sub>, if we let the topic corresponding to this word be t<sub>j</sub>, we can rewrite the above formula as:</p><p><em>p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>) &#x3D; p(w<sub>i</sub>|t<sub>j</sub>)*p(t<sub>j</sub>|d<sub>s</sub>)</em></p><p>Let’s ignore how this value is calculated for now (you can first understand it as directly taking the corresponding terms from θ<sub>ds</sub> and φ<sub>tj</sub>. Actually it’s not that simple, but it doesn’t affect understanding the overall LDA process, which will be discussed later.). Now we can enumerate all topics in T, obtaining all p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>), where j ranges from 1 to k. Then based on these probability values, we can select a topic for the i-th word w<sub>i</sub> in d<sub>s</sub>. The simplest approach is to choose the t<sub>j</sub> that maximizes p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>) (note that j is the only variable in this expression), i.e.,</p><p><em>argmax[j] p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>)</em></p><p>Of course, this is just one method (and it doesn’t seem to be commonly used). In practice, there are many methods in academia for choosing t here, which I haven’t thoroughly researched yet.</p><p>Then, if the i-th word w<sub>i</sub> in d<sub>s</sub> selects a different topic than before, it will affect θ<sub>d</sub> and φ<sub>t</sub> (as can be easily seen from the calculation formulas of these two vectors mentioned earlier). Their effects will in turn affect the calculation of p(w|d) mentioned above. Performing one calculation of p(w|d) for all w in all d in D and reselecting topics is considered one iteration. After n such iterations, the result required by LDA will converge. <em>[Here I suddenly thought of a question: whether the updates to θ<sub>d</sub> and φ<sub>t</sub> are done uniformly after updating all w in all d in one iteration (meaning within one iteration, θ<sub>d</sub> and φ<sub>t</sub> remain unchanged, and are updated uniformly at the end of the iteration), or whether θ<sub>d</sub> and φ<sub>t</sub> are updated immediately after each w in each d has its topic updated? This seems to require looking at the original LDA paper.]</em></p><h4 id="How-to-Calculate-p-w-t-and-p-t-d"><a href="#How-to-Calculate-p-w-t-and-p-t-d" class="headerlink" title="How to Calculate p(w|t) and p(t|d)"></a>How to Calculate p(w|t) and p(t|d)</h4><p>To be continued…</p><p>&nbsp;</p><h4 id="References"><a href="#References" class="headerlink" title="References"></a>References</h4><h2 id="1-Introduction-to-Latent-Dirichlet-Allocation-A-foreign-blog-a-great-introductory-article"><a href="#1-Introduction-to-Latent-Dirichlet-Allocation-A-foreign-blog-a-great-introductory-article" class="headerlink" title="[1] Introduction to Latent Dirichlet Allocation: A foreign blog, a great introductory article"></a>[1] <a href="http://blog.echen.me/2011/08/22/introduction-to-latent-dirichlet-allocation/">Introduction to Latent Dirichlet Allocation</a>: A foreign blog, a great introductory article</h2><p>Source: <a href="https://lichuanyang.top/en/posts/63299/">https://lichuanyang.top/en/posts/63299/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/63299/</id>
    <link href="https://lichuanyang.top/en/posts/63299/"/>
    <published>2012-12-02T07:20:00.000Z</published>
    <summary>LDA (Latent Dirichlet Allocation) topic model introductory learning notes, helping to understand the basic concepts and application scenarios of LDA.</summary>
    <title>A Great Introduction to the LDA Model</title>
    <updated>2026-06-27T03:53:05.196Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="big-data" scheme="https://lichuanyang.top/en/tags/big-data/"/>
    <category term="hadoop" scheme="https://lichuanyang.top/en/tags/hadoop/"/>
    <content>
      <![CDATA[<h2 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h2><p>Reference: <a href="http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html">http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html</a></p><p>&nbsp;</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.apache.hadoop.examples;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.StringTokenizer;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.conf.Configuration;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.fs.Path;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.io.IntWritable;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.io.Text;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.mapreduce.Job;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.mapreduce.Mapper;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.mapreduce.Reducer;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.mapreduce.lib.input.FileInputFormat;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.hadoop.util.GenericOptionsParser;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WordCount</span> &#123;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="Mapper-Implementation"><a href="#Mapper-Implementation" class="headerlink" title="Mapper Implementation"></a>Mapper Implementation</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">　　<span class="keyword">public</span> <span class="keyword">static</span> class **TokenizerMapper**</span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">extends</span> <span class="title class_">Mapper</span>&lt;Object, Text, Text, IntWritable&gt;&#123; <span class="comment">// Inherits from org.apache.hadoop.mapreduce.Mapper class and overrides its map method</span></span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="type">IntWritable</span> <span class="variable">one</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">IntWritable</span>(<span class="number">1</span>);  <span class="comment">// **Mapper&lt;KEYIN,VALUEIN,KEYOUT,VALUEOUT&gt;**</span></span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">private</span> Text &lt;span style=<span class="string">&quot;color: #ff0000;&quot;</span>&gt;word&lt;/span&gt; = <span class="keyword">new</span> <span class="title class_">Text</span>();</span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">map</span><span class="params">(Object key, Text value, Context context)</span>  <span class="comment">// Called once for each key/value pair in the input split</span></span><br><span class="line"></span><br><span class="line">　　　　　　　　<span class="keyword">throws</span> IOException, InterruptedException &#123; <span class="comment">// value stores one line of the text file (terminated by newline), while key is the offset of the first character of that line relative to the beginning of the text file</span></span><br><span class="line"></span><br><span class="line">　　　　　　　　<span class="type">StringTokenizer</span> <span class="variable">itr</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringTokenizer</span>(value.toString());    <span class="comment">// Split into words</span></span><br><span class="line"></span><br><span class="line">　　　　　　　　<span class="keyword">while</span> (itr.hasMoreTokens()) &#123;</span><br><span class="line"></span><br><span class="line">　　　　　　　　&lt;span style=<span class="string">&quot;color: #ff0000;&quot;</span>&gt;word&lt;/span&gt;.set(itr.nextToken());</span><br><span class="line"></span><br><span class="line">　　　　　　　　context.write(word, one);  <span class="comment">// Output &lt;word, 1&gt;</span></span><br><span class="line"></span><br><span class="line">　　　　　　　&#125;</span><br><span class="line"></span><br><span class="line">　　　　　&#125;</span><br><span class="line"></span><br><span class="line">　　　&#125;</span><br><span class="line"></span><br><span class="line">　　&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// The system automatically sorts the map results, etc. The reduce input example: (asd,1-1-1)</span></span><br></pre></td></tr></table></figure><h2 id="Reducer-Implementation"><a href="#Reducer-Implementation" class="headerlink" title="Reducer Implementation"></a>Reducer Implementation</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">　　<span class="keyword">public</span> <span class="keyword">static</span> class **IntSumReducer**</span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">extends</span> <span class="title class_">Reducer</span>&lt;Text,IntWritable,Text,IntWritable&gt; &#123; <span class="comment">// Reducer&lt;KEYIN,VALUEIN,KEYOUT,VALUEOUT&gt;</span></span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">private</span> <span class="type">IntWritable</span> <span class="variable">result</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">IntWritable</span>();</span><br><span class="line"></span><br><span class="line">　　　　　　<span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">reduce</span><span class="params">(Text key, Iterable&lt;IntWritable&gt; values,Context context)</span></span><br><span class="line"></span><br><span class="line">　　　　　　　　　　 <span class="keyword">throws</span> IOException, InterruptedException &#123;  <span class="comment">// Reducer input is the Map process output; &lt;key,values&gt; where key is a single word, and values are the count values for that word</span></span><br><span class="line"></span><br><span class="line">　　　　　　　　<span class="type">int</span> <span class="variable">sum</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">　　　　　　　　<span class="keyword">for</span> (IntWritable val : values) &#123;</span><br><span class="line"></span><br><span class="line">　　　　　　　　　　　sum += val.get();</span><br><span class="line"></span><br><span class="line">　　　　　　　　&#125;</span><br><span class="line"></span><br><span class="line">　　　　　　result.set(sum);</span><br><span class="line"></span><br><span class="line">　　　　　　context.write(key, result);</span><br><span class="line"></span><br><span class="line">　　　　　&#125;</span><br><span class="line"></span><br><span class="line">　　　&#125;</span><br><span class="line"></span><br><span class="line">　　&#125;</span><br><span class="line"></span><br><span class="line">　　&#125;</span><br></pre></td></tr></table></figure><h2 id="Main-Method"><a href="#Main-Method" class="headerlink" title="Main Method"></a>Main Method</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">　　<span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> **main**(String[] args) <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"></span><br><span class="line">　　　　<span class="type">Configuration</span> <span class="variable">conf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Configuration</span>();</span><br><span class="line"></span><br><span class="line">　　　　String[] otherArgs = <span class="keyword">new</span> <span class="title class_">GenericOptionsParser</span>(conf, args).getRemainingArgs();</span><br><span class="line"></span><br><span class="line">　　　　<span class="keyword">if</span> (otherArgs.length != <span class="number">2</span>) &#123;</span><br><span class="line"></span><br><span class="line">　　　　　　System.err.println(<span class="string">&quot;Usage: wordcount &lt;in&gt; &lt;out&gt;&quot;</span>);</span><br><span class="line"></span><br><span class="line">　　　　　　System.exit(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line">　　　　&#125;</span><br><span class="line"></span><br><span class="line">　　　　<span class="type">Job</span> <span class="variable">job</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Job</span>(conf, <span class="string">&quot;word count&quot;</span>);</span><br><span class="line"></span><br><span class="line">　　　　job.setJarByClass(WordCount.class);</span><br><span class="line"></span><br><span class="line">　　　　job.setMapperClass(TokenizerMapper.class); <span class="comment">// setMapperClass: Set the Mapper, defaults to IdentityMapper</span></span><br><span class="line"></span><br><span class="line">　　　　job.setCombinerClass(IntSumReducer.class);</span><br><span class="line"></span><br><span class="line">　　　　job.setReducerClass(IntSumReducer.class);<span class="comment">// setReducerClass: Set the Reducer, defaults to IdentityReducer</span></span><br><span class="line"></span><br><span class="line">　　　　job.setOutputKeyClass(Text.class);</span><br><span class="line"></span><br><span class="line">　　　　job.setOutputValueClass(IntWritable.class);</span><br><span class="line"></span><br><span class="line">　　　　FileInputFormat.addInputPath(job, <span class="keyword">new</span> <span class="title class_">Path</span>(otherArgs[<span class="number">0</span>]));/ FileInputFormat.addInputPath: Set the input file path; can be a file, a path, or a wildcard. Can be called multiple times to add multiple paths</span><br><span class="line"></span><br><span class="line">　　　　FileOutputFormat.setOutputPath(job, <span class="keyword">new</span> <span class="title class_">Path</span>(otherArgs[<span class="number">1</span>]));/ FileOutputFormat.setOutputPath: Set the output file path; <span class="built_in">this</span> path should not exist before the job runs</span><br><span class="line"></span><br><span class="line">　　　　System.exit(job.waitForCompletion(<span class="literal">true</span>) ? <span class="number">0</span> : <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Other-Configuration-Notes"><a href="#Other-Configuration-Notes" class="headerlink" title="Other Configuration Notes"></a>Other Configuration Notes</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// setInputFormat: Set the map input format, defaults to TextInputFormat, key is LongWritable, value is Text</span></span><br><span class="line"></span><br><span class="line">setNumMapTasks: Set the number of map tasks. This setting usually doesn<span class="string">&#x27;t take effect; the number of map tasks depends on the number of input splits the input data can be divided into</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">setMapRunnerClass: Set the MapRunner. Map tasks are run by the MapRunner, which defaults to MapRunnable. Its function is to read records from input splits one by one and sequentially call the Mapper&#x27;</span>s map function</span><br><span class="line"></span><br><span class="line">setMapOutputKeyClass and setMapOutputValueClass: Set the key-value pair format of the Mapper<span class="string">&#x27;s output</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">setOutputKeyClass and setOutputValueClass: Set the key-value pair format of the Reducer&#x27;</span>s output</span><br><span class="line"></span><br><span class="line">setPartitionerClass and setNumReduceTasks: Set the Partitioner, which defaults to HashPartitioner. It decides which partition a <span class="keyword">record</span> <span class="title class_">enters</span> based on the hash value of the key. Each partition is processed by one reduce task, so the number of partitions equals the number of reduce tasks</span><br><span class="line"></span><br><span class="line">setOutputFormat: Set the task<span class="string">&#x27;s output format, which defaults to TextOutputFormat</span></span><br></pre></td></tr></table></figure><hr><p>Source: <a href="https://lichuanyang.top/en/posts/25187/">https://lichuanyang.top/en/posts/25187/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/25187/</id>
    <link href="https://lichuanyang.top/en/posts/25187/"/>
    <published>2012-10-28T10:42:00.000Z</published>
    <summary>Classic Hadoop WordCount example code with runtime explanation, essential for getting started with big data.</summary>
    <title>WordCount Code</title>
    <updated>2026-06-27T03:48:22.986Z</updated>
  </entry>
  <entry>
    <author>
      <name>SandGrid</name>
    </author>
    <category term="Big Data" scheme="https://lichuanyang.top/en/categories/Big-Data/"/>
    <category term="big-data" scheme="https://lichuanyang.top/en/tags/big-data/"/>
    <category term="hadoop" scheme="https://lichuanyang.top/en/tags/hadoop/"/>
    <content>
      <![CDATA[<blockquote><p>⚠️ These are study notes from <strong>2012</strong>. The Hadoop ecosystem has changed dramatically over the past decade. The links below are very outdated. I’ve preserved the original content as a historical record and added 2026-era learning recommendations.</p></blockquote><span id="more"></span><h2 id="Why-Learn-About-Hadoop-in-2026"><a href="#Why-Learn-About-Hadoop-in-2026" class="headerlink" title="Why Learn About Hadoop in 2026?"></a>Why Learn About Hadoop in 2026?</h2><p>You might wonder: with Spark, Flink, and data lakes everywhere, why bother with Hadoop?</p><p>The answer is simple: <strong>HDFS and YARN remain the foundation of big data infrastructure</strong>. Spark runs on YARN by default, Hive table data lives in HDFS, and even many cloud-native data platforms use HDFS-compatible storage under the hood. Understanding Hadoop’s core design — distributed file systems, compute resource scheduling, data locality — helps you better grasp the design decisions of upper-layer frameworks.</p><h2 id="Recommended-Learning-Path-for-2026"><a href="#Recommended-Learning-Path-for-2026" class="headerlink" title="Recommended Learning Path for 2026"></a>Recommended Learning Path for 2026</h2><h3 id="Getting-Started-Core-Concepts"><a href="#Getting-Started-Core-Concepts" class="headerlink" title="Getting Started: Core Concepts"></a>Getting Started: Core Concepts</h3><ol><li><strong>HDFS</strong>: Understand the fundamentals of a distributed file system — NameNode&#x2F;DataNode architecture, data blocks, replication, rack awareness</li><li><strong>MapReduce</strong>: Learn the map-shuffle-reduce computation model and understand why it excels at batch processing but struggles with iterative workloads</li><li><strong>YARN</strong>: Resource management and scheduling — the concepts of ApplicationMaster and Container</li></ol><p>Recommended reading: <a href="https://hadoop.apache.org/docs/stable/">Hadoop Official Documentation</a> (latest 3.x release)</p><h3 id="Leveling-Up-From-Theory-to-Practice"><a href="#Leveling-Up-From-Theory-to-Practice" class="headerlink" title="Leveling Up: From Theory to Practice"></a>Leveling Up: From Theory to Practice</h3><p>Rather than writing raw MapReduce programs from scratch, I recommend jumping straight into Spark:</p><ul><li><strong>Spark</strong>: Faster than MapReduce, with a much friendlier API. Supports SQL&#x2F;DataFrame&#x2F;Streaming&#x2F;MLlib across multiple paradigms</li><li><strong>Hive</strong>: SQL on Hadoop — transforms SQL queries into distributed compute jobs, ideal for analytics use cases</li></ul><h3 id="Going-Deeper-Architectural-Evolution"><a href="#Going-Deeper-Architectural-Evolution" class="headerlink" title="Going Deeper: Architectural Evolution"></a>Going Deeper: Architectural Evolution</h3><p>A few key changes across Hadoop 1.x → 2.x → 3.x are worth understanding:</p><table><thead><tr><th>Version</th><th>Major Changes</th></tr></thead><tbody><tr><td>1.x</td><td>Basic HDFS + MapReduce, single NameNode (single point of failure)</td></tr><tr><td>2.x</td><td>Introduced YARN (resource management decoupled), NameNode HA, Federation</td></tr><tr><td>3.x</td><td>Erasure Coding (storage savings), YARN Timeline Service v2, GPU scheduling support</td></tr></tbody></table><hr><h2 id="📚-Original-Notes-Compiled-in-2012"><a href="#📚-Original-Notes-Compiled-in-2012" class="headerlink" title="📚 Original Notes (Compiled in 2012)"></a>📚 Original Notes (Compiled in 2012)</h2><p>The following links were collected when I first started learning Hadoop. Most of them are likely broken or outdated — kept here for historical reference only:</p><ul><li><a href="http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html">WordCount Execution Process Detailed Explanation</a> — The classic entry point for understanding MapReduce</li><li><a href="http://www.cnblogs.com/gpcuster/archive/2010/06/04/1751538.html">HDFS Command Introduction</a> — Basic file operation commands</li><li><a href="http://hi.baidu.com/gkf8605/item/d6b8af09c3463512eafe38b1">HDFS Commands (another reference)</a> — Supplementary command reference</li><li><a href="http://www.cnblogs.com/forfuture1978/archive/2010/11/14/1877086.html">MapReduce Introduction</a> — MapReduce programming model basics</li><li><a href="http://hadoop.apache.org/docs/r0.20.2/api/index.html">Hadoop 0.20.2 API</a> — API docs for version 0.20.2</li></ul><blockquote><p>Back in 2012, when I first encountered Hadoop, there was no Spark, no data lakes, no Kubernetes. MapReduce <em>was</em> big data processing in its entirety — even Hive was in its early days. Technology always advances faster than expected, but the fundamentals are always worth learning.</p></blockquote><h2 id="Frequently-Asked-Questions"><a href="#Frequently-Asked-Questions" class="headerlink" title="Frequently Asked Questions"></a>Frequently Asked Questions</h2><h3 id="Q-Do-I-still-need-to-learn-Hadoop-in-2026"><a href="#Q-Do-I-still-need-to-learn-Hadoop-in-2026" class="headerlink" title="Q: Do I still need to learn Hadoop in 2026?"></a>Q: Do I still need to learn Hadoop in 2026?</h3><p>Yes, but you don’t need to go as far as writing MapReduce programs. HDFS and YARN remain the foundation of big data infrastructure — Spark runs on YARN by default, Hive data lives in HDFS, and even many cloud-native data platforms use HDFS-compatible storage under the hood. Understanding core concepts like distributed file systems, resource scheduling, and data locality helps you better grasp the design decisions of upper-layer frameworks.</p><h3 id="Q-Where-should-I-start-with-Hadoop"><a href="#Q-Where-should-I-start-with-Hadoop" class="headerlink" title="Q: Where should I start with Hadoop?"></a>Q: Where should I start with Hadoop?</h3><p>I recommend a three-step approach: <strong>Stage 1 — Foundations</strong>: understand HDFS (NameNode&#x2F;DataNode architecture, replication) and YARN (resource management) at the conceptual level. <strong>Stage 2 — Practice</strong>: jump straight into Spark for data processing rather than grinding through MapReduce programming. <strong>Stage 3 — Going deeper</strong>: learn about the architectural evolution from Hadoop 1.x → 2.x → 3.x, understanding why features like Erasure Coding and NameNode Federation were introduced.</p><h3 id="Q-Is-there-value-in-learning-outdated-technologies"><a href="#Q-Is-there-value-in-learning-outdated-technologies" class="headerlink" title="Q: Is there value in learning outdated technologies?"></a>Q: Is there value in learning outdated technologies?</h3><p>Yes, but distinguish between “learning the principles” and “learning the operations.” Operational details from ten years ago do become obsolete (like version-specific API calls), but design principles often stand the test of time. MapReduce’s shuffle model still exists in Spark today. HDFS’s replication mechanism and rack-awareness design can be seen echoed in cloud storage architectures. The key is to extract timeless design insights from dated implementations.</p><h3 id="Q-With-Spark-around-do-I-still-need-MapReduce"><a href="#Q-With-Spark-around-do-I-still-need-MapReduce" class="headerlink" title="Q: With Spark around, do I still need MapReduce?"></a>Q: With Spark around, do I still need MapReduce?</h3><h2 id="You-don’t-need-to-learn-MapReduce-programming-specifically-but-understanding-its-computation-model-is-worthwhile-MapReduce’s-map-shuffle-reduce-pipeline-data-locality-optimizations-and-fault-tolerance-mechanisms-—-these-ideas-are-carried-forward-and-improved-in-Spark-Understanding-why-MapReduce-was-slow-is-the-best-way-to-truly-grasp-how-Spark-achieves-speed-through-in-memory-computation-DAG-scheduling-and-RDD-lineage-Each-of-these-optimizations-is-a-direct-response-to-MapReduce’s-shortcomings"><a href="#You-don’t-need-to-learn-MapReduce-programming-specifically-but-understanding-its-computation-model-is-worthwhile-MapReduce’s-map-shuffle-reduce-pipeline-data-locality-optimizations-and-fault-tolerance-mechanisms-—-these-ideas-are-carried-forward-and-improved-in-Spark-Understanding-why-MapReduce-was-slow-is-the-best-way-to-truly-grasp-how-Spark-achieves-speed-through-in-memory-computation-DAG-scheduling-and-RDD-lineage-Each-of-these-optimizations-is-a-direct-response-to-MapReduce’s-shortcomings" class="headerlink" title="You don’t need to learn MapReduce programming specifically, but understanding its computation model is worthwhile. MapReduce’s map-shuffle-reduce pipeline, data locality optimizations, and fault tolerance mechanisms — these ideas are carried forward and improved in Spark. Understanding why MapReduce was slow is the best way to truly grasp how Spark achieves speed through in-memory computation, DAG scheduling, and RDD lineage. Each of these optimizations is a direct response to MapReduce’s shortcomings."></a>You don’t need to learn MapReduce programming specifically, but understanding its computation model is worthwhile. MapReduce’s map-shuffle-reduce pipeline, data locality optimizations, and fault tolerance mechanisms — these ideas are carried forward and improved in Spark. Understanding <em>why</em> MapReduce was slow is the best way to truly grasp <em>how</em> Spark achieves speed through in-memory computation, DAG scheduling, and RDD lineage. Each of these optimizations is a direct response to MapReduce’s shortcomings.</h2>]]>
    </content>
    <id>https://lichuanyang.top/en/posts/35575/</id>
    <link href="https://lichuanyang.top/en/posts/35575/"/>
    <published>2012-10-28T10:05:00.000Z</published>
    <summary>Hadoop learning resources collection, including detailed WordCount execution process explanation, HDFS command introduction, and other introductory resources.</summary>
    <title>Hadoop Basic Learning Resources</title>
    <updated>2026-06-27T02:40:58.911Z</updated>
  </entry>
</feed>
