[{"content":"今天发现，我前几年完全无法理解的多头注意力模型，被 Astra 大人拆碎喂过来后，我能理解了！\n词表 首先，我们需要针对语料拆个词表出来。比如 BPE，反复寻找经常相邻出现的两个片段，把它们合成一个新片段。\n简化例子：\n订单成交 订单取消 查询订单 订单过期 先按单字拆：\n订 | 单 | 成 | 交 订 | 单 | 取 | 消 查 | 询 | 订 | 单 订 | 单 | 过 | 期 然后算法会计算得到，“订单”连在一起 4 次，那么就把“订单”也加入表里，拆成：\n订单 | 成 | 交 订单 | 取 | 消 查 | 询 | 订单 订单 | 过 | 期 如果还有别的语料，可能还会合并出“成交”“取消”“过期”“查询”等片段，直到达到设定的条件为止。\n然后我们的输入，就可以这么按词表拆分成一个个 token。\nEmbedding 接下来，这些 token（tokenId）进入 Transformer 的 Embedding 层，从一张可训练的向量表中取出对应行，得到向量。\n这个 tokenId 和对应的向量，是一一对应的映射。经过这一层，一句话被分词为 token 列表，然后变成 token 向量列表。\nq、k、v 然后就正式开始了 Transformer 循环。在第一次循环里，每个 token 都要乘以三个权重矩阵 Qw、Kw、Vw，得到向量 q、k、v。\n注意，这一步和其他 token 没有关系，所以可以并行计算。\n这三个向量还挺哲学的：\nv：我是谁（内容）\nk：我从哪里来（我被谁需要）\nq：我到哪里去（我需要什么）\n乘 Vw乘 Kw乘 Qw当前 token 的向量v：我是谁（内容）k：我从哪里来（我被谁需要）q：我到哪里去（我需要什么）注意力计算 然后再把 q、k、v 一起计算注意力，那个公式简单说就是：\n当前位置的 q 分别与每个允许参考位置的 k 做点积。比如我们一共有 10 个 token，正在计算第 5 个时，我们要把 q₅ 和前 5 个 token 的 k 进行点积计算。 于是对每个 token 都打了一个分数，共有 5 个分数。这个分数还得除以根号 d，也就是 q、k、v 维度数的根号。还是拿 512 维、8 头注意力举例，这里的 q、k、v 维度是 64，所以我们要除以 √64，也就是 8。 然后对这一组分数做 Softmax，得到参考比例，按比例对各个位置对应的 v 加权计算，就得到了新的向量。 当前位置的 q₅分别做点积，得到 5 个分数允许参考的 k₁～k₅除以 √64，也就是 8Softmax：得到参考比例按比例加权求和对应位置的 v₁～v₅新的向量位置编码与 RoPE 这里其实还缺失了一点，就是 token 的位置信息我们没提到。\n在最原版的 Transformer 里，位置信息是直接在 token 向量上加个固定向量，然后丢进 Transformer 循环的。\n但在 RoPE 里，是在 q、k、v 计算完成后，对 q、k 进行旋转来完成位置信息编码的。\n假如 q 或者 k 向量为 (1, 0)，那么不同的位置会将它旋转不同角度：\n位置 旋转角度 旋转后的向量 0 0° (1, 0) 1 90°（这个度数可配置） (0, 1) 2 90° × 2 (-1, 0) … … … 这一步真的是太神奇了。我们都知道，q 要乘以其他 token 的 k。\n这就意味着，保持旋转前的 q、k 不变，只把它们的位置同时向后移动相同距离，旋转后的 q、k 乘积都不会变。比如两个向量原来是 (1, 0)、(1, 0)：\n所在位置 旋转后的两个向量 乘积 0、2 (1, 0)、(-1, 0) -1 1、3 (0, 1)、(0, -1) -1 … … … 只考虑相对位置，不考虑绝对位置。\n但平面就 360 度，而且实际 token 维度可能为 512 甚至更多，这么高维度怎么转呢？\n解法太精妙了：两个维度一组，分别旋转，且旋转速度不同。这样就可以保留相对位置乘积参数不变的同时，让 512 维度都参与进来。多种旋转速度结合起来，可以提供更多的位置信息空间。\n只要这个位置信息没有丢失，那么模型训练过程中，自然会利用到这个信息，啧。\n多头注意力 然后 q、k、v 再用那个注意力公式，就得到了和 v 一样长度的新向量。其他 token 也一样并行计算，就得到了一组新向量。\n以上是单头注意力。我们只要把这个计算用新的一组权重算 n 次，就得到 n 头注意力。\n我们还是假设 token 是 512 维，然后 q、k、v 是 64 维的（调整权重矩阵即可得到不同维度）。我们的单头注意力就是 64 维，只要算 8 次得到 8 头注意力，然后把这 8 头连接，即可得到 8 × 64 = 512 维的新向量。\n然后这 512 维度，再乘一个输出矩阵 W，让这多头信息混合起来。然后再残差处理，然后进行 FNN，再残差、归一化等等，这些传统神经网络操作一波，最后输出 512 维的新向量。\ntoken 向量：512 维头 1：64 维头 2：64 维……头 8：64 维拼接：8 × 64 = 512 维乘输出矩阵 W，混合多头信息Transformer 块内部残差残差＋多头注意力FNN＋归一化输入向量 · 512 维输出向量 · 512 维预测下一个 token 接下来就是下一组循环，把这一批 512 维的向量再输入到下一个 Transformer 里，然后再下一组，再下一组。\n到最后一组循环，我们取最后一个向量。这时候这个向量已经和前面的向量产生了足够多的纠缠了（我为什么会用纠缠这个词，人的意识不会来自量子层面的纠缠吧？其实原理和这个差不多？瞎猜的。）。\n我们用这个向量来预测下一个向量，把它丢进一个分类预测网络层里。这个层的输出和词表大小一样，每个维度代表每一个 token 的概率。我们一般取最大的，或者再 Softmax 一下，然后按概率随机一下之类的，得到下一个 token。\n预测输出Transformer 多层堆叠词表中各 token 的概率分类预测网络下一个 tokenTransformer 块 1Transformer 块 2Transformer 块 Ntoken 向量序列最后一个位置的向量MoE 然后还有 MoE 专家模型，也很简单。\n在多头注意力计算之后的传统神经网络里，我们可以准备多套权重（多个专家），每次进行一次路由。路由规则是一层分类网络，参与训练，输出一组后面应该激活的专家概率，选择最高的那几个专家即可。\n然后这些专家分别预测新的向量，按专家概率把它们加起来即可。\n多头注意力计算之后的向量路由：分类网络给出专家概率选择概率最高的几个专家专家 1 预测新向量专家 2 预测新向量……按专家概率加起来新的向量啧。\n","permalink":"/posts/transformer-notes/","summary":"今天发现，我前几年完全无法理解的多头注意力模型，被 Astra 大人拆碎喂过来后，我能理解了！\n词表 首先，我们需要针对语料拆个词表出来。比如 BPE，反复寻找经常相邻出现的两个片段，把它们合成一个新片段。\n简化例子：\n订单成交 订单取消 查询订单 订单过期 先按单字拆：\n","title":"Transformer 解析"},{"content":" 原始写作：2026-10-12；本站整理：2026-10-12。\n翻棋联机是一款可以和朋友直接开局的暗棋游戏：无需注册，一人创建房间，另一人加入，双方准备后开始对局。项目同时提供原生 Android 客户端和适配手机、电脑的网页版，两个端可以进入同一房间。\n这篇文章介绍项目目前的联机方式、部署方法和自动构建流程。项目地址：LinHuiG/chinese-chess-flipping。\n从一盘暗棋开始 棋盘采用 4×8 布局，包含翻棋、走子、特殊吃法和阵亡统计。每步可以选择 30、60、90 秒或不限时，游戏内可查看完整规则，重复棋面会被限制。房间名和昵称都可自行设置。\nAndroid 客户端最低支持 Android 8.0，可以从服务器网页下载 APK，也可以从 GitHub Actions 最近一次成功发布的 app-update 附件获取安装包。网页版打开服务器网站即可游玩。项目 README\nAndroid 直连，网页经服务器联机 服务端使用 Rust，Android 客户端使用原生 Java，网页资源随服务端一起提供。\nAndroid 玩家之间可以尝试 UDP 直连；直连失败时自动回到服务器中转。网页版通过服务器联机。因此 P2P 的适用范围是 Android 玩家之间，跨端联机仍有服务器参与。\n断线恢复依赖仍然有效的会话。会话保留到最后一次有效心跳后 60 秒，短暂断网、App 重启或网页刷新后可以恢复；断线期间回合倒计时继续。服务端重启会清空在线房间，需要重新创建或加入。这几个条件决定了“能恢复”与“需要重开”的边界。项目 README\n一个镜像带上服务端、网页和安装包 Docker 镜像同时包含 Rust 服务端、网页和 Android 安装包，支持 linux/amd64 与 linux/arm64。运行镜像不需要另装 Java，也不需要配置数据库。\n下载项目的 compose.yaml，在同一目录创建 .env：\nSERVER_IMAGE=ghcr.io/linhuig/chinese-chess-flipping:latest TCP_PORT=8888 UDP_PORT=8888 HTTP_PORT=80 HTTP_BIND_ADDRESS=0.0.0.0 HTTP_PUBLISH_PORT=8080 TCP_WORKER_THREADS=2 然后拉取镜像并启动：\ndocker compose pull docker compose up -d --no-build docker compose logs --tail=50 server 浏览器访问 http://服务器地址:8080。Android 可以选 HTTP 并使用端口 8080，也可以选 TCP 并使用 8888。\n对外端口 用途 TCP 8080 网页、WebSocket 联机、App 更新 TCP 8888 Android TCP 连接 UDP 8888 协助 Android 玩家建立直连 TCP 和 UDP 需要分别配置防火墙与安全组。UDP 不可达时仍可通过服务器中转；修改 UDP 端口时，容器内外端口应保持一致。公网部署可配置 HTTPS/WSS；同机反向代理场景下，将 HTTP_BIND_ADDRESS 改为 127.0.0.1，再由代理转发网页与 WebSocket 请求。部署说明\n自动构建先检查，再发布 发布工作流分为客户端与服务端检查、两种架构的镜像发布，以及多架构清单合并。合并请求执行检查；满足范围条件的主分支推送或手动运行才发布镜像。\n流程先准备 Android APK 与版本清单，执行客户端逻辑检查、Lint、Rust 测试与构建；随后启动服务端，检查网页资源、更新接口以及需要执行的客户端与服务端互通测试，并运行网页恢复与协议检查。发布包使用固定签名，合并请求使用调试签名。\n工作流按构建输入缓存已经检查过的产物，缓存命中后还会核对 APK 清单、Rust 文件哈希和互通验证标记，再决定哪些构建与检查可以复用。网页与协议检查仍在流程中执行。\n镜像分别在 amd64 和 arm64 运行器上构建。每种架构先按 digest 上传，再启动容器核对版本清单、网页脚本和 APK；验证完成后更新架构标签。两个架构都成功后，才把本次构建的两个 digest 合成多架构清单。合并时使用确定的 digest，避免从可变的 latest 标签取到别次构建的产物。发布工作流\n本地开发和后续更新 本地开发需要 Rust 稳定版工具链，以及 README 指定的 Android Studio、JDK、SDK 和 Build Tools。服务端启动需要 APK 与版本清单，因此先构建 Android 客户端，再运行打包脚本，最后启动服务端。调试包与正式包签名不同，不能直接覆盖安装。\n常用检查命令如下，在仓库根目录执行：\ncargo test --locked --manifest-path server/Cargo.toml .\\android_client\\gradlew.bat -p android_client lintDebug testDebugUnitTest node scripts/session-v2-test.mjs 互通测试需要先启动本地服务端并设置对应测试端口；未设置时会跳过互通测试。日常部署更新只需重新拉取镜像并运行 docker compose up -d --no-build，但更新会中断当前对局，应在无人游玩时进行。客户端开发与消息格式可继续阅读 通信协议。\n","permalink":"/posts/chess-online/","summary":" 原始写作：2026-10-12；本站整理：2026-10-12。\n翻棋联机是一款可以和朋友直接开局的暗棋游戏：无需注册，一人创建房间，另一人加入，双方准备后开始对局。项目同时提供原生 Android 客户端和适配手机、电脑的网页版，两个端可以进入同一房间。\n","title":"翻棋联机：Android、网页与 Docker 自动构建"},{"content":" 旧作补记\n原始时间：2026-09-20\n本站补录：2026-10-12。\n橘白色的猫猫是五月份的时候来上班的，主人四月初从一窝毛球里相中了它，那时候它眼睛都没完全睁开。\n主人说，让猫妈妈再养大一些吧。\n上班后，猫猫有猫粮吃，也有猫条和罐罐。生病了，可以去专门为猫猫建的医院。\n工作很简单，主人想摸摸的时候过去贴贴就好了。主人也有自己的工作，猫猫大部分时间都在窗边晒太阳。\n晒太阳很舒服，主人专门在窗边铺了一层绒垫子，垫子暖暖的，太阳也暖暖的。\n窗外有虫子和麻雀飞过，太阳和月亮轮流来看它，猫猫觉得，自己已经上了好久的班。\n没来由地，它突然想起了当时那一窝毛球，主人说它长得最好看，那一窝毛球只有它收到了offer。\n主人还说，它来上班的那天，猫妈妈被原来养她的主人解雇了。被解雇的猫猫，就要自己去抓老鼠吃了。\n它透过玻璃往下看，楼下水泥地被阳光晒得晃眼，这里会有老鼠吗？\n猫猫在绒垫子上翻了个身，换个姿势眯上了眼，空调凉凉的，舒服。\n","permalink":"/posts/cat-at-work/","summary":" 旧作补记\n原始时间：2026-09-20\n本站补录：2026-10-12。\n橘白色的猫猫是五月份的时候来上班的，主人四月初从一窝毛球里相中了它，那时候它眼睛都没完全睁开。\n主人说，让猫妈妈再养大一些吧。\n上班后，猫猫有猫粮吃，也有猫条和罐罐。生病了，可以去专门为猫猫建的医院。\n","title":"猫猫来上班"},{"content":" 旧作补记\n原始时间：约 2026 年 9 月，具体日期不详\n本站补录：2026-10-12。\n换工作电脑时，文件复制成功，只能说明文件到了目标目录。真正的验收要回到平常的操作：项目能否打开并构建、SSH 能否从实际使用的客户端连接、历史资料与新增资料是否都在。\n先分清需要搬什么 把材料分成程序、配置、用户数据、附件、缓存和凭据。不同类别有不同的恢复方式。程序可以重新安装；配置要适配路径；用户数据要防止覆盖新内容；缓存通常可以重建；机器绑定的凭据可能必须通过应用支持的方式迁移或重新登录。\n只按“安装过的软件”列清单容易遗漏便携程序、托盘工具、快捷键和启动项。它们体积很小，却可能直接影响每天的使用习惯。\n修路径之前，确认应用实际读哪里 用户目录、盘符和启动身份变化后，应用可能仍读旧位置，也可能在退出时把内存中的配置重新写回。检查日志、实际修改时间和应用界面，再判断哪个路径生效。不要仅凭一个环境变量就批量替换全部路径。\nSSH 的配置文件可能保留旧的 IdentityFile、Include 或代理程序路径。修复时保留现有密钥和主机身份校验，分别验证命令行入口与 IDE 入口。某个终端能连接，不能证明 IDE 也已加载新配置。\n对正在变化的数据，先取得一致副本 聊天记录、会话索引或其他 SQLite 数据可能包含尚未合入主文件的更新。应使用应用支持的导出、冷备份或 SQLite 的受支持备份机制，不能只复制一个正在被写入的数据库文件就宣布完成。SQLite Backup API 文档\n新电脑已经产生新记录时，要先保留它们，再在副本上比较和合并。文件完整性、旧记录覆盖、当前新记录、附件可打开，是不同的验收项。\n开发环境按项目原来的版本恢复 先确认项目 Wrapper、SDK 和缓存目录。迁移路径与升级构建工具应分开处理，否则新增的不兼容会掩盖原来的路径问题。版本命令成功、配置加载成功、编译成功、IDE 同步成功，也应分别记录。\n一份能复查的验收单 项目 文件层面 实际操作层面 IDE SDK、项目配置与缓存路径 同步并编译代表项目 SSH 配置引用与权限 从平时的入口认证 历史数据 一致副本、记录范围 打开代表性旧记录与新记录 小工具 程序目录与启动项 快捷键、托盘和自动启动 本次笔记的经验是：按实际工作流验收，比按复制进度验收更有效。清理旧备份要放在验收之后；无法证明重复的唯一资料继续保留。\n可复用的迁移 skill 把恢复步骤、常见误判和验收标准整理成一份 skill，换机时可以按同一套要求逐项处理。内容包括主文件 SKILL.md 和配套的 references/pitfalls.md，两份文件需要保留原有目录关系。\n下载完整 skill 包 · 查看 SKILL.md · 查看专项处理要点\n下面附上完整内容。使用时应先确认本次机器、路径与操作范围，不能把旧案例中的环境信息直接套到新机器。\nSKILL.md name: windows-profile-migration description: 恢复或验收Windows换机迁移，处理盘符映射、用户配置、开发工具链、聊天数据库与附件、机器绑定凭据，以及迁移后的有证据冗余清理。 Windows用户环境迁移 适用于旧机到新机或不同用户配置之间的恢复。保持用户指定的软件、版本、路径、延后/取消范围；使用前获取本次机器实际信息，不沿用参考案例的盘号、用户名和目录。\n开始前确认的事实 哪台机器在运行，源备份和目标卷是谁；确认卷标、容量、硬件身份，不能只看盘符。 用户已验收、正在使用、取消、暂缓及明确禁止操作的应用。用户自行修好的应用不再纳入自动修复。 区分配置、用户数据、附件、缓存、凭据存储；盘符映射必须覆盖项目内配置、快捷方式、启动项和虚拟机列表。 旧机尚可用时，优先核实官方跨机导出、机器密钥、凭据管理器和恢复验证，之后才能建议退役旧机。 SSH不能只复制密钥：核对实际客户端及HOME、config中的旧用户/盘符引用和有效Host匹配，再在授权范围内用禁止密码交互且保留主机身份校验的方式验证认证。命令行成功与IDE拉取成功分开记录。 不只盘点安装程序：同时检查便携工具、托盘/任务栏组件、登录启动项、全局快捷键、hosts和环境变量；发现候选后结合用户范围筛选，不盲目恢复所有启动项。 优先级与带宽 建立单一安装恢复队列，记录用户优先级、已有安装包、下载体积、前置依赖、需交互环节、暂缓/取消状态。先恢复关键工作能力，低优先级大下载不应占满关键任务带宽。受登录、缺盘阻塞的单项不阻塞其他可执行项。用户说“继续”时重新处理暂缓范围，不能遗忘之前的工作。\n低带宽是持续约束：以实测速率估时，间隔采样文件字节数并结合进程/错误日志判断。下载未结束不等于卡死；有持续增长就保留下载，只有明确异常证据才重试。用户提供安装包后核验本地包并防止重复任务。\n恢复与验收 读取应用日志、进程信息和实际写入文件确定生效目录。MSIX、注册表/文件虚拟化可能让工具子进程与桌面启动看到不同目录；不要仅根据APPDATA推断。 恢复前备份目标当前数据。应用会在退出时写回配置时，等待用户退出或用应用支持的快照机制，避免与用户新修改竞争。 SQLite使用受支持的备份或冷备份，关联WAL/SHM一起考虑。新旧数据共存时，在副本中按主键与schema合并，新记录冲突策略应明确。原库损坏时先定位可读范围，保留原件和失败证据。 跨盘复制禁止默认镜像删除。去重备份必须用映射重建原目录。对已存在目标先比较内容；不同内容保留双方，不能仅凭时间戳或文件大小覆盖。 配置文件已写入、应用已加载、代表功能验证、用户验收是不同状态。只报告已获得的证据，不把quick_check、文件数量或服务存在当作完整功能恢复。 按用户工作流做分项验收：IDE全局配置与项目同步；会话工具的原始会话、索引、项目注册和归属；浏览器的个人资料、密码、扩展、书签和旧标签页；聊天工具的消息、新增数据与附件；小工具的快捷键、显示和启动项。第一次失败后梳理相关层面的证据缺口，不只修复当前症状。 hosts恢复先备份新机文件，比较有效条目并检查冲突后合并，再验证名称解析；系统代理和环境变量亦应比较后更新，避免整体覆盖新机设置。 遇到凭据、聊天数据、开发工具链或运行身份问题时，读取专项处理要点。\n下载、安装及开发工具链 从官方来源确认安装方式、兼容性、版本和签名；不同安装器的静默参数和路径引用规则不能套用。 低网速下依据文件增长和进程判断进度，不因等待久而重启或重复下载。用户提供安装包后优先验证该包。 Gradle Wrapper、项目指定版本和原可用组合优先于IDE默认最新版；分清Gradle安装目录、用户缓存目录、Gradle JVM和项目语言级别。 先做版本/离线配置加载检查，依赖解析或编译单独记录。不运行发布、生产服务或用户数据变更来验证迁移。 收尾清理 用户授权清理且验收完成后：\n建立具体文件清单，标注大小、保留副本、哈希证据和删除理由。先形成计划，再执行。 仅清理确认重复的安装包、可再生失败下载、被最终验证产物替代的工作副本等。不能把“旧”或“大”当删除理由。 保留唯一原始备份、无法再导出的机器凭据资料、用户新数据，以及用户要求保留的脚本/文档。无法确认重复的备份保留并说明。 删除前重新校验文件未变化、解析后的路径在授权工作区内、无链接越界；优先逐文件删除，不拼接跨shell递归删除命令。 输出已删文件与实际释放的逻辑字节数，不承诺等同磁盘可用空间变化。清理后更新单一权威验收单，旧进度保留为历史。 技能不授予额外权限；遵守本会话的应用操作、联网、键鼠、重启及外部写入边界。\nreferences/pitfalls.md 专项处理要点 SSH文件已恢复但仍要求密码 先区分服务器账号密码、私钥口令和主机身份确认。访问.ssh被权限拒绝不等于密钥不存在；按权限流程核查真实用户目录，不输出私钥。\n检查实际Git/IDE所用SSH客户端与HOME，使用有效配置输出确认Host别名、用户、端口、IdentityFile和IdentitiesOnly。迁移后IdentityFile可能仍引用旧用户绝对路径；优先在适用时使用用户相对路径，不盲目全局替换。Include、IdentityAgent和ProxyCommand也可能引用旧路径。重复Host配置按实际匹配结果分析，不未经确认删除。\n备份config后只修复已证实错误的项，保留已有密钥、known_hosts及私钥权限。经授权可用BatchMode=yes、StrictHostKeyChecking=yes与连接超时做只认证测试；不关闭主机身份校验，不重新生成密钥来掩盖路径问题。成功欢迎信息证明该客户端可认证，不代表IDE已加载新配置或拉取成功；后续按用户实际入口验证。\n机器绑定凭据 连接名、URL、驱动、账号、密码分别检查。密文完整不代表新机可解密；应使用应用官方跨机导出/导入或正常账号登录。原系统及密钥不可用时明确边界，不能保证解密。用户授权读取本地配置或配置平台时，按环境、主机、端口、库/schema和账号交叉匹配；同IP不是充分条件，历史验证也不代表当前可用。不打印密码、不把秘密写入复盘或技能、不擅自替换账号。\n聊天数据库 先证明应用正在使用哪个库。附件路径与消息数据库路径经常独立。用户已使用新机时，必须保留新消息；冷备份或受支持快照后在副本操作。校验schema、索引、完整性、旧消息覆盖率、新消息数量及内容一致性。对修复后的库用独立连接检查，避免把ATTACH的旧损坏库报错归咎于新库。部分表损坏应披露范围，不推断附件文件缺失等于消息缺失。\nMSIX与运行身份 工具环境的APPDATA/HKCU可能与普通桌面应用不同。通过实际日志、路径与更新时间验证；显式配置路径必须有应用加载证据。若需普通用户会话的计划任务执行，经授权后用最低所需权限，完成即移除临时任务。不把启动环境推断当作已证明的故障原因。\nIDE项目迁移 检查项目.idea、Wrapper、项目局部缓存设置和SDK表中的旧盘符。IDE运行时可能保留内存设置，外部改文件不一定加载；提示用户重载，避免反复覆盖。界面里“Gradle用户主目录”与“本地安装目录”是两个概念。旧build.gradle可能使用新版已删除的配置，不能以升级Gradle替代路径修复。版本检查成功、help成功、编译成功和IDE同步成功分别验收。\n慢网速与安装 下载记录保存PID、来源、已写大小、预期大小和校验信息；重试前确认没有已有有效下载。安装包签名验证后归档并比较哈希，勿重复安装已完成的软件。免费驱动可能没有静默安装能力；按用户键鼠边界启动交互向导，再核验服务与接口枚举。枚举接口不等同实际抓包，不擅自记录用户网络流量。\n失败模式到验收证据 |失败模式|需要的证据| |文件复制完但历史为空|应用实际库路径、代表性旧记录、用户确认| |会话在而项目不在|原始会话与项目注册/分组分别恢复并验收| |数据库连接存在却全失效|机器凭据迁移路径与真实认证结果| |IDE版本冲突|实际Gradle路径、JVM、项目原版本及IDE重载结果| |任务栏工具有文件没显示|进程、显示配置、当前用户启动项、界面确认| |迁移进度文档相互矛盾|最终权威验收单与保留的历史更正记录|\n会话工具与浏览器 会话文件、索引、项目注册、项目目录和会话归属是独立恢复项；复制原始会话并不能保证重启后侧栏和项目都恢复。针对具体版本查明支持的恢复方式，保护当前会话和用户新数据，不固定依赖某次迁移发现的内部表结构。\n浏览器逐项验收个人资料、密码可用性、扩展及必要配置、书签、旧标签页。跨机器受保护的密钥不能靠整目录复制保证可用；优先支持的同步/导出。历史会话恢复时避免覆盖用户已开始使用的新配置。\n容易漏掉的日常环境 便携目录、托盘工具、启动项、全局快捷键、任务栏组件都可能不出现在常规安装清单。根据旧机实际使用证据和用户偏好识别；不要等用户凭记忆逐个描述后才补。检查自动启动的用户身份与路径，进程存在不等于任务栏可见或快捷键有效。\nhosts备份存在、有效条目已合并、域名解析生效是三个检查点。先确认文件编码和真实大小；结构化输出避免PowerShell混合对象格式化导致误读。日志和复盘不输出公司内部秘密。\n安装形成下载—签名校验—安装—功能验收—归档的一条流程，避免用户反复追问。同一问题多次失败时重新检查实际目录、运行身份、内存配置和关联数据，而不是重复同一复制操作。\n","permalink":"/posts/windows-migration/","summary":" 旧作补记\n原始时间：约 2026 年 9 月，具体日期不详\n本站补录：2026-10-12。\n换工作电脑时，文件复制成功，只能说明文件到了目标目录。真正的验收要回到平常的操作：项目能否打开并构建、SSH 能否从实际使用的客户端连接、历史资料与新增资料是否都在。\n","title":"Windows 换机：从文件复制到实际工作流验收"},{"content":" 旧作补记\n原始时间：约 2026 年 9 月，具体日期不详\n本站补录：2026-10-12。\n文中测试数据为原始测试记录，本次补录未重新压测。\n1. 背景与设计目标 由于多种原因，消息可能延迟处理，例如，10:00 产生的消息在 10:10 才被消费，若使用消费时间生成消息号 messageId，其排序位置就会偏离原始消息时间。双机房部署下，两地消息顺序不一致更容易引起注意。\n本方案以 Kafka 消息时间为基准生成 messageId，尽可能保证延迟消费后的消息列表在宏观上按时间有序。 秒内分配顺序和溢出处理可能产生局部偏移，因此不保证严格的逐条时间顺序，也不要求同一消息在两机房生成完全相同的 messageId。\n存储与推送时效分别处理：通过业务过滤且需要存储的消息先写入 Redis，再判断是否允许推送。超过默认 15 分钟推送时限的消息仍可用于补齐列表，但不再实际推送。\n2. 设计思路 概括：算法基于雪花算法“时间 + 节点号 + 序列号”的组合思路进行改进：采用 Kafka 消息时间 保持排序基准，采用 秒级状态缓存 控制内存开销，采用 步长 200 满足 Redis score 的精度约束。\n正常分配时，编号由以下部分相加得到：\nmessageId = (消息所属秒 + 溢出偏移秒数) × 10,000,000 + 节点号 × 1,000,000 + 秒内序号 × 200 + 621355968000000000 每秒的节点编号空间划分为 10 个节点区间，节点号为 0～9。每个节点占用 1,000,000 个数值单位，按步长 200 分配，可容纳 5000 个编号，序号范围为 0～4999。纪元常量和时间计算中的加 8 小时处理用于兼容历史 messageId 格式；无溢出时，偏移秒数为 0。\n2.1 秒级粒度的选择 内存开销。 算法需要缓存全天各时间段的分配状态。按秒需要 86,400 个状态槽，按毫秒需要 86,400,000 个；采用相同结构时，毫秒级的槽存储开销约为秒级的 1000 倍。对于宏观时间保序的业务要求，秒级粒度能够以较低内存成本满足需求。\n分配容量。 舍弃原始毫秒部分后，秒内空间统一用于节点区间和序号分配，避免容量分散在各毫秒内，更适合集中消费和突发流量。\n2.2 步长 200 的选择 Redis ZSet 使用 double 保存排序分数。当前 messageId 数值较大，差值过小的两个 long 编号在转换后可能得到相同的 score。\n当前 messageId 量级下，double 相邻可表示值的间距为 128。选择步长 200，可使正常分配的相邻编号转换为 score 后仍可区分，但不代表每个 messageId 都能被 double 精确表示。\n舍弃毫秒和设置步长是两项配合的设计：前者用于组织容量、节省内存，后者直接解决步长过小导致 score 相同的问题。\n3. 分配流程示例 算法为一天中的每一秒维护一个状态槽。消息根据时间戳定位所属秒槽，并在该槽中申请序号。\n假设 10:00:00 对应的基础编号为 B，该节点这一秒尚未分配过：\n第 1 条：B 第 2 条：B + 200 第 3 条：B + 400 …… 第 5000 条：B + 999800 第 5001 条：转到 10:00:01 对应的秒槽继续分配 当前秒槽容量耗尽后，继续查找后续秒槽。借用后续秒仅涉及编号空间分配，无需等待实际时间推进。 溢出消息与原本属于目标秒的消息共用该槽计数，避免重复分配。\n5000 表示单个秒槽的容量，不是实时处理吞吐量上限。溢出后 messageId 的时间部分会向后偏移，因此保序能力以宏观时间顺序为目标。\n4. 关键代码解析 函数位于 MessageIdGenerator.nextIdBySecond。下面按执行顺序拆解；代码片段省略了部分注释和外围括号。\n4.1 状态初始化与时间定位 private static final AtomicLong[] SECOND_STATE_SLOTS = new AtomicLong[24 * 60 * 60]; private static final int STEP = 200; private static final int MAX_OFFSET = 1000000 / STEP; static { for (int i = 0; i \u003c SECOND_STATE_SLOTS.length; i++) SECOND_STATE_SLOTS[i] = new AtomicLong(0); } 数组为全天各秒保存分配状态，通过 AtomicLong 支持并发条件更新。静态初始化执行一次，进程重启后状态清零。\nfinal long mills = currentMills + 8 * 60 * 60 * 1000; final long sec = mills / 1000; final int dayIndex = (int) (sec / 86400); final int secondOfDay = (int) (sec % 86400); long packedSlot, newPackedSlot, targetSec = secondOfDay; long storedDay, overflowSec, allocOffset; int retryCount = 0; AtomicLong offset; currentMills 接收 Kafka 时间戳。加 8 小时后除以 1000，得到秒级时间，毫秒部分被舍弃。\ndayIndex 表示第几天，secondOfDay 表示当天第几秒。例如 10:00:03 对应索引 36003。每天都会复用这个位置，所以必须记录日期，才能判断计数是否需要重置。\ntargetSec 是当前分配位置，初始值为 secondOfDay，容量耗尽后向后移动。packedSlot 和 newPackedSlot 分别保存读取状态与候选更新状态；storedDay、overflowSec、allocOffset 分别表示槽日期、跳转位置和待分配序号。\n4.2 状态读取与字段拆分 offset = SECOND_STATE_SLOTS[(int) targetSec]; packedSlot = offset.get(); storedDay = (packedSlot \u003e\u003e\u003e 40) \u0026 0xFFFFFF; overflowSec = (packedSlot \u003e\u003e\u003e 20) \u0026 0xFFFFF; allocOffset = packedSlot \u0026 0xFFFFF; 每个 long 按 24/20/20 位布局保存三个字段：\n高 24 位：日期 | 中间 20 位：溢出跳转位置 | 低 20 位：待分配序号\n\u003e\u003e\u003e 将需要的部分右移，\u0026 只保留指定的位。例如提取日期时，先右移 40 位，再保留 24 位。三个字段都来自同一次读取，后面也可以一次性更新，避免日期和计数分别修改造成状态不一致。\noverflowSec=0 表示未建立跳转。allocOffset=7 表示序号 0～6 已分配，本次尝试分配序号 7。\n4.3 日期比较与状态重置 if (storedDay \u003e dayIndex) { return sec * 10000000 + nodeId * 1000000 + 621355968000000000L; } if (storedDay \u003c dayIndex) { storedDay = dayIndex; overflowSec = allocOffset = 0; newPackedSlot = (storedDay \u003c\u003c 40) + 1; } 如果槽里已经是更新的日期，例如槽属于 9 月 15 日，消息却来自 9 月 14 日，代码直接返回不带序号偏移的 messageId。这个分支会放弃唯一性，同秒同节点可能重复。\n如果消息日期更新，则清除旧计数和跳转，从序号 0 开始。storedDay « 40 将日期写入高位，+1 将下一待分配序号设为 1。\n本次使用序号 0，候选状态记录下一序号 1。 此处仅构造新状态，后续 CAS 成功后才确认分配。\n4.4 同日分配与溢出跳转 else if (overflowSec == 0) { if (allocOffset \u003e= MAX_OFFSET) { overflowSec = targetSec + 1; newPackedSlot = packedSlot | (overflowSec \u003c\u003c 20); } else { newPackedSlot = packedSlot + 1; } } else newPackedSlot = 0; 当前槽容量未满时，候选计数加 1。例如读取值为 7，本次使用 7，更新值为 8。正常计数不超过 5000，加法不会进位到日期或跳转字段。\n达到 5000 时，0～4999 已用完，就把下一个槽的位置写入中间 20 位。| 将跳转信息合入原状态，保留日期和计数。此操作只建立跳转，仍需进入目标槽完成序号分配。\n如果已经有跳转，就不再修改当前槽。newPackedSlot=0 表示跳过本轮更新，沿现有指向继续找，不是把数组状态清零。\n4.5 CAS 更新与循环控制 } while ((newPackedSlot \u003e 0 \u0026\u0026 !offset.compareAndSet(packedSlot, newPackedSlot)) || (overflowSec \u003e 0 \u0026\u0026 (targetSec = overflowSec) \u003c SECOND_STATE_SLOTS.length \u0026\u0026 retryCount++ \u003c 16)); compareAndSet，简称 CAS，意思是：只有槽里的状态仍等于刚才读到的值，才写入新值。 如果已被其他线程修改，就失败并重试。\n例如两个线程同时读取计数 7，并尝试更新为 8，只有一个线程能够成功分配序号 7；另一个重新读取状态后，继续申请后续序号。\n这段循环有两个继续条件：更新失败，就留在当前槽重新读；存在跳转，就移动到后面的槽继续找。左侧更新失败时，右侧不会执行，所以 retryCount 只限制溢出跳转，不限制所有 CAS 重试。\n目标到达 86400，或跳转计数达到限制时，循环也会退出。这两种退出不代表已成功分配序号，属于后文列出的边界情况。\n4.6 溢出偏移与跳转路径优化 long secOffset = targetSec - secondOfDay; if (retryCount \u003e 2 \u0026\u0026 secOffset \u003e 1) { packedSlot = SECOND_STATE_SLOTS[secondOfDay].get(); overflowSec = (packedSlot\u003e\u003e\u003e 20) \u0026 0xFFFFF; if (overflowSec \u003c targetSec) { SECOND_STATE_SLOTS[secondOfDay].compareAndSet( packedSlot, ((long) dayIndex \u003c\u003c 40) | (targetSec \u003c\u003c 20) | MAX_OFFSET); } } secOffset 表示目标秒相对原消息秒的偏移。例如目标从 10:00:00 移至 10:00:03，偏移为 3 秒。\n若经过多个满槽，就尝试把原槽直接指向较远的位置：\n原来：10:00:00 → 10:00:01 → 10:00:02 → 10:00:03 优化：10:00:00 → 10:00:03 候选状态保留本次日期、更新直达位置，并将原槽计数标记为已满。此处仅优化后续查找路径，CAS 失败无需重试，正常情况下不影响已分配编号。\n4.7 messageId 组合与返回 return (sec + secOffset) * 10000000 + nodeId * 1000000 + STEP * allocOffset + 621355968000000000L; 返回值由目标秒、节点区间、序号偏移和纪元常量组成。例如节点 2 分配序号 7，对应的秒内偏移为 2,000,000 + 7 × 200。\n无溢出时使用原消息秒，溢出时增加 secOffset。完整流程为：定位秒槽 → 原子分配序号 → 必要时向后溢出 → 组合 messageId。\n5. 实现边界 宏观保序，不等于逐条编号一致。 同秒消息在两个机房的处理顺序或节点号不同，messageId 就可能不同。借用后续秒也会带来局部偏移，不能从 messageId 还原精确产生时间。\n唯一性依赖进程状态与节点配置。 状态保存在进程内。旧日期消息命中已被新日期复用的槽、同节点进程重启回放，或者多个进程使用相同节点号，都可能产生重复编号。节点号 0～9 的范围也依赖配置，函数未主动校验。\n极端溢出不保证正常分配。 最后一秒用满或跳转次数达到限制时，可能在未完成目标槽分配时直接返回。路径优化也没有再次校验原槽日期，跨日并发回放需注意。当前实现并未解决这些边界。\n存储保证受业务规则与写入结果约束。 消息仍需通过业务过滤、启用存储并成功写入 Redis；编号函数不负责写入失败后的重试和补偿。\n源码依据：编号生成函数与消息存储、分发流程。\n6. 测试情况 测试内容包括运行槽位分析、APP 日志编号分析和多线程生成性能测试。\n6.1 运行槽位分析 将监控快照中的 86,400 个槽位按日期、分配数量和溢出指针解码。槽位按日期逐项复用，因此快照中的分配数量是各槽位当前保留量，不能直接当作某一天的完整处理量。\n检查非空槽、满槽、溢出槽以及后续首个未满槽，并完整追踪跳转链。单个指针跨度、整条链的时间跨度与跳转次数分别统计；链的时间跨度不等于循环次数。\n原始检查中，全部槽位计数均不超过 5,000，溢出指针均向后且在数组范围内。具体生产日期、流量统计和业务时段不在本文展示。\n6.2 日志消息编号分析 按编号统计出现次数、节点号、步长对齐情况，再与监控快照的槽位分配范围逐秒比较，检查是否存在超出分配范围的编号。\n分时日志按写入时间归档，编号却按消息时间生成。因此一个日志文件可能缺少边界附近的部分编号，同时包含相邻时间区间的编号。核对相邻文件后，才能区分日志切割差异与编号生成异常。\n原始检查中的重复记录来自同一次推送的多个处理阶段：开始、处理中与完成都携带同一编号。日志重复记录不等于编号重复生成，应先按一次处理流程归并，再判断生成器是否重复分配。\n6.3 多线程生成性能测试 测试环境为 Intel Core i7-11700F、16 个逻辑核、Windows、JDK 1.8.0_151。直接调用 MessageIdGenerator.nextIdBySecond，固定节点编号为 1。\n每组预热 200 万次后测试 3 轮，每轮约 1,000 万次，共完成 72 轮、720,002,400 次生成。每个线程使用独立预分配的 long[] 记录 ID；线程创建和数组分配在计时前完成，合并、排序、校验及写盘在计时后完成。计时区间内 GC 次数为 0。\n场景 时间戳分配方式 完全无秒槽竞争 每个线程独占不同秒槽，每槽最多生成 4,000 个 ID，无溢出 共享 64 个起始秒 所有线程轮转访问 64 个起始秒 共享 8 个起始秒 所有线程轮转访问 8 个起始秒 共享同一秒 所有线程使用同一个起始时间戳 共享场景的起始秒间隔为 512 秒，用于隔离不同热点的溢出区间。\n6.3.1 生成吞吐 单位为 百万 ID/秒，取三轮中位数，统计生成调用与线程内数组写入的整体吞吐。\n线程数 完全无秒槽竞争 共享 64 个起始秒 共享 8 个起始秒 共享同一秒 10 572.39 143.43 57.42 12.45 50 703.92 195.82 49.21 11.62 100 585.60 183.81 47.20 11.92 300 490.76 171.73 48.16 11.79 500 474.13 171.84 49.47 11.45 1000 258.11 132.54 48.24 11.52 完全无竞争时，吞吐为 2.58～7.04 亿 ID/秒；共享同一秒时，吞吐为 1,145～1,245 万 ID/秒。热点越集中，吞吐下降越明显。以上为固定工作量的短批次测量，无竞争档单轮耗时约 14～41 毫秒。\n6.3.2 编号校验 以下为每组三轮累计的 多余重复次数 / 节点越界条数，各轮独立校验。\n线程数 完全无秒槽竞争 共享 64 个起始秒 共享 8 个起始秒 共享同一秒 10 0 / 0 0 / 0 0 / 0 0 / 0 50 0 / 0 0 / 7 51 / 389 612 / 2,073 100 0 / 0 0 / 35 78 / 259 618 / 1,997 300 0 / 0 0 / 0 0 / 0 670 / 1,359 500 0 / 0 0 / 0 0 / 0 0 / 4 1000 0 / 0 0 / 0 0 / 0 0 / 6 测试结果符合预期。完全无秒槽竞争的各组均无重复和节点越界。共享场景触达跳转上限时，累计出现 2,029 次多余重复、6,129 条节点越界记录，符合已接受的边界行为。步长校验通过，未发现不同整数 ID 转为 Redis double 后的额外碰撞。全部生成记录按线程独立保存，落盘数据复核结果与压测统计一致。\n6.4 测试总结 原始测试包括运行槽位检查、客户端日志编号分析，以及 10～1000 线程、约 7.2 亿次生成的测试。实际日志中的重复项已确认为同一次推送的多阶段记录，小时边界差异由日志切割造成，未发现实际样本中的编号生成异常。\n短批次压测中，完全无秒槽竞争时吞吐约 2.58～7.04 亿 ID/秒，共享同一秒时约 1,145～1,245 万 ID/秒。无竞争场景编号校验通过，共享场景触达跳转上限后的表现符合已接受的边界行为。\n整体测试结果符合预期。\n","permalink":"/posts/message-id-slots/","summary":" 旧作补记\n原始时间：约 2026 年 9 月，具体日期不详\n本站补录：2026-10-12。\n文中测试数据为原始测试记录，本次补录未重新压测。\n1. 背景与设计目标 由于多种原因，消息可能延迟处理，例如，10:00 产生的消息在 10:10 才被消费，若使用消费时间生成消息号 messageId，其排序位置就会偏离原始消息时间。双机房部署下，两地消息顺序不一致更容易引起注意。\n","title":"根据时间戳生成消息号算法设计"},{"content":" 旧作补记\n原始时间：2026-07-21\n本站补录：2026-10-12。\n刷到小红书上一张把日常话语逐行写成诗的帖子，于是我也自己写了一首。\n闲\n摸鱼\n看手机\n又没消息\n蝉声撞着窗\n云朵遮住太阳\n雨后空气很清新\n但那之前沉闷苦涩\n行人把午后踩得匆忙\n魔幻的城市在低声呼吸\n突然黑浪在屋脊翻涌低鸣\n一方玻璃托住整座倾斜的雨\n屋檐下信鸽飞过衔走半片阴影\n忽有一束晴光落进掌心里的纹路\n风停叶静\n天光破云\n","permalink":"/posts/xian/","summary":" 旧作补记\n原始时间：2026-07-21\n本站补录：2026-10-12。\n刷到小红书上一张把日常话语逐行写成诗的帖子，于是我也自己写了一首。\n闲\n摸鱼\n看手机\n又没消息\n蝉声撞着窗\n云朵遮住太阳\n雨后空气很清新\n但那之前沉闷苦涩\n行人把午后踩得匆忙\n魔幻的城市在低声呼吸\n突然黑浪在屋脊翻涌低鸣\n一方玻璃托住整座倾斜的雨\n屋檐下信鸽飞过衔走半片阴影\n忽有一束晴光落进掌心里的纹路\n风停叶静\n天光破云\n","title":"闲"},{"content":" 旧作补记\n原始时间：2026-07-04\n本站补录：2026-10-12。\n前几天看到一位网友吐槽《壁上观》的歌词写得不好，我看了一下，确实不怎么样。所以今天练力量之余，还试着用 AI 填了版词。 我不停地审核、驳回、提意见，AI 就不停地干活，Token 都快给我烧没了。本来还想让 AI 直接帮我唱，但 Codex 搞了半天也没搞成，最后还是我自己弄了个半成品出来。歌词放在下面。 曲子就不放了。过几年估计会有更好用的 AI，到时候再让新的 AI 唱。\n填词 长安春风 入千重石龛\n宝相初开 飞袖连天\n胡旋琵琶动 照云烟\n一壁歌舞上诸天\n驼铃随落日 穿过关山\n画师落笔 敷彩成莲\n千灯入梦 梵音无言\n尽收盛唐入岩龛\n一刹那 风沙为人间 阖上旧卷\n错把千秋作梦眠\n藏经洞 千年沉寂后 忽启尘关\n庚子烽烟压九天\n龟甲辞 与西陲残卷 同证河山\n千载伏脉此朝还\n万卷书 飘零关山外 犹照人间\n暗夜里自见山川\n守窟长灯明 新元开篇\n残彩重苏 云袖回旋\n寸寸修回那 旧云烟\n诸天眉眼又安然\n万屏开卷照 万里关山\n云端展帙 星海相连\n壁上神女 终赴星汉\n一梦飞入九重天\n若来日 别有遗珍藏 尘烟之间\n休问灯火几时还\n尚有卷 无名沉幽处 静待成篇\n待一灯照破重关\n石门忽 重启尘封处 又见云天\n方知失去未断弦\n尘开处 千年伏线仍 暗自相牵\n因缘早伏尘寰间\n逐句说明 长安春风 入千重石龛\n（以“长安春风”引出盛唐气象，“千重石龛”落到莫高窟，将王朝繁华吹入石壁。）\n宝相初开 飞袖连天\n（“宝相”写佛像庄严，“飞袖”写乐舞轻灵，一静一动，铺开壁画的神圣与华美。）\n胡旋琵琶动 照云烟\n（胡旋舞与琵琶声并起，写出丝路乐舞交融；“照云烟”让壁画仿佛被声色点亮。）\n一壁歌舞上诸天\n（把满壁乐舞升格为通向诸天的景象，既是艺术，也是信仰。）\n驼铃随落日 穿过关山\n（驼铃、落日、关山写丝路辽阔，暗示敦煌并非孤立石窟，而是文明交汇之地。）\n画师落笔 敷彩成莲\n（画师一笔落下，色彩化作莲华，写艺术创造，也写信仰供养。）\n千灯入梦 梵音无言\n（“千灯”写香火与岁月，“梵音无言”写壁画沉默，却自有回响。）\n尽收盛唐入岩龛\n（第一段收束：莫高窟把盛唐的信仰、审美、乐舞和气象一并收藏进岩壁。）\n一刹那 风沙为人间 阖上旧卷\n（时间骤转，风沙像替历史合上书卷，盛唐繁华进入漫长封存。）\n错把千秋作梦眠\n（千年沉寂如梦，写埋藏之久，也写后人一度遗忘。）\n藏经洞 千年沉寂后 忽启尘关\n（藏经洞在沉寂千年后开启，“尘关”既是沙尘封门，也是历史之门。）\n庚子烽烟压九天\n（点出庚子乱局，战争阴云压顶，与藏经洞开启形成强烈对照。）\n龟甲辞 与西陲残卷 同证河山\n（以甲骨与藏经洞并举，写文明证据在危难时刻相继现身，证明来路未断。）\n千载伏脉此朝还\n（那些埋在时间深处的文脉，在这一刻重新浮现。）\n万卷书 飘零关山外 犹照人间\n（写经卷流散之痛，但“犹照人间”说明即使离散，文明光芒仍在。）\n暗夜里自见山川\n（在最黑暗的时代，反而照见自身山河与文化根脉。）\n守窟长灯明 新元开篇\n（转入新纪元，“守窟长灯”写一代代保护者接续守护。）\n残彩重苏 云袖回旋\n（壁画残色被修复，飞天云袖仿佛重新转动。）\n寸寸修回那 旧云烟\n（“寸寸”写修复之细与艰难，也写把昔日光华一点点唤回。）\n诸天眉眼又安然\n（修复后的诸天形象重新安定清明，也象征文化记忆得到安放。）\n万屏开卷照 万里关山\n（写数字化传播，敦煌不再只在洞窟中，而能被万屏共看。）\n云端展帙 星海相连\n（“云端”点出现代技术，“星海”引出航天想象，古老卷帙与未来相连。）\n壁上神女 终赴星汉\n（点出“飞天成真”：壁画中的飞天意象，在新时代化为真正奔赴星空的能力。）\n一梦飞入九重天\n（把千年飞天之梦推向高潮，既浪漫，也写现实成就。）\n若来日 别有遗珍藏 尘烟之间\n（主题打开：未来也许还有别的文明遗珍暂时沉入尘烟。）\n休问灯火几时还\n（不急问何时重见天日，因为文明有自己的时间。）\n尚有卷 无名沉幽处 静待成篇\n（那些未被命名、未被发现的遗存，仍在幽暗里等待被重新书写。）\n待一灯照破重关\n（一盏灯象征发现、研究与守护，照开下一道历史关门。）\n石门忽 重启尘封处 又见云天\n（又一次开启尘封之处，历史重新看见天空，文明再次显影。）\n方知失去未断弦\n（所谓失去并非彻底断绝，只是文脉一度隐入暗处。）\n尘开处 千年伏线仍 暗自相牵\n（尘埃散开时，才看见千年之间早有线索相连。）\n因缘早伏尘寰间\n（收束全词：发现看似偶然，其实是历史、文明与时代共同酝酿出的必然。）\n立意总结 这首词以莫高窟壁画为核心，从盛唐繁华写到千年封存，再写庚子乱世中藏经洞与甲骨相继显现，表达文明在最危急、最失重的时刻反而显出根脉。随后转入新纪元的守护、修复、数字化与飞天成真，说明古老文化并非只供凭吊，而能成为未来的精神底气。最后把视野推向尚未发现的遗珍：一切重逢看似偶然，实则都有千年伏脉暗中相牵。\n","permalink":"/posts/bishangguan/","summary":" 旧作补记\n原始时间：2026-07-04\n本站补录：2026-10-12。\n前几天看到一位网友吐槽《壁上观》的歌词写得不好，我看了一下，确实不怎么样。所以今天练力量之余，还试着用 AI 填了版词。 我不停地审核、驳回、提意见，AI 就不停地干活，Token 都快给我烧没了。本来还想让 AI 直接帮我唱，但 Codex 搞了半天也没搞成，最后还是我自己弄了个半成品出来。歌词放在下面。 曲子就不放了。过几年估计会有更好用的 AI，到时候再让新的 AI 唱。\n","title":"《壁上观》填词：千年伏脉"},{"content":" 旧文迁移 · 补录说明\n原始发表：2023-08-10；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\nimport java.util.Arrays; import java.util.Comparator; import java.util.Scanner; public class Main { public static void main(String[] args) { G(); } static int n; static long S; static long mod = 998244353; static Edge[] a = new Edge[200005]; static int[] bcj = new int[200005]; static long[] bcjNum = new long[200005]; private static void G() { Scanner in = new Scanner(System.in); int t = in.nextInt(); for (int i = 0; i \u003c 200005; i++) { a[i] = new Edge(); } while (t-- \u003e 0) { n = in.nextInt(); S = in.nextInt(); for (int i = 0; i \u003c n - 1; i++) { a[i].u = in.nextInt() - 1; a[i].v = in.nextInt() - 1; a[i].w = in.nextInt(); bcj[i] = i; bcjNum[i] = 1; } bcj[n - 1] = n - 1; bcjNum[n - 1] = 1; Arrays.sort(a, 0, n - 1, Comparator.comparingLong(o -\u003e o.w)); long sum = 1; for (int i = 0; i \u003c n - 1; i++) { Edge edge = a[i]; if (edge.w \u003e= S) break; int uFather = getBcj(edge.u); int vFather = getBcj(edge.v); long uNum = bcjNum[uFather]; long vNum = bcjNum[vFather]; bcjNum[uFather] += bcjNum[vFather]; bcj[vFather] = uFather; long temp = uNum * vNum - 1; temp %= (mod-1); sum *= quickPow(S - edge.w + 1, temp, mod); sum %= mod; } System.out.println(sum); } } private static int getBcj(int index) { return bcj[index] == index ? index : (bcj[index] = getBcj(bcj[index])); } private static long quickPow(long a, long n, long mod) { long ans = 1; a %= mod; while (n \u003e 0) { if ((n \u0026 1) \u003e 0) {//如果n的当前末位为1 ans *= a; //ans乘上当前的a ans %= mod; } a *= a; //a自乘 a %= mod; n \u003e\u003e= 1; //n往右移一位 } return ans; } private static class Edge { int u, v, w; } private static void F() { Scanner in = new Scanner(System.in); int t = in.nextInt(); int n, q; long x, y; long[] a = new long[200005]; while (t-- \u003e 0) { n = in.nextInt(); for (int i = 0; i \u003c n; i++) { a[i] = in.nextLong(); } Arrays.sort(a, 0, n); q = in.nextInt(); while (q-- \u003e 0) { x = in.nextLong(); y = in.nextLong(); long tt = x * x - 4 * y; if (tt \u003c 0) { System.out.print(0); System.out.print(' '); continue; } long tq = sqrt(tt); long ai = x - tq; long aj = x + tq; ai /= 2; aj /= 2; if (ai + aj != x || ai * aj != y) { System.out.print(0); System.out.print(' '); continue; } long aiCount = getCount(a, n, ai); if (aiCount == 0) { System.out.print(0); System.out.print(' '); continue; } if (ai == aj) { System.out.print(aiCount * (aiCount - 1) / 2); System.out.print(' '); continue; } long ajCount = getCount(a, n, aj); System.out.print(aiCount * ajCount); System.out.print(' '); } System.out.println(); } } private static long sqrt(long n) { long low, high, mid, tmp; // 获取上下界 if (n \u003c= 1) { return n; } low = 1; high = Math.min(n, 3000000000L); // 二分法求开方 while (low \u003c= high) { mid = (low + high) / 2; tmp = mid * mid; if (tmp == n) { return mid; } else if (tmp \u003e n) { high = mid - 1; } else { low = mid + 1; } } return 0; } private static int getCount(long[] a, int n, long num) { int lIndex = getLeftIndex(a, 0, n - 1, num); int rIndex = getRightIndex(a, 0, n - 1, num); return rIndex - lIndex + 1; } private static int getLeftIndex(long[] a, int from, int end, long num) { int l = from, r = end, mid = 0; while (l \u003c= r) { mid = l + r; mid /= 2; if (a[mid] \u003c num) { l = mid + 1; } else { r = mid - 1; } } return l; } private static int getRightIndex(long[] a, int from, int end, long num) { int l = from, r = end, mid = 0; while (l \u003c= r) { mid = l + r; mid /= 2; if (a[mid] \u003c= num) { l = mid + 1; } else { r = mid - 1; } } return r; } private static class Data { long val; long valSumLitterThanVal; int index; long result; } private static void E() { Scanner in = new Scanner(System.in); int t = in.nextInt(); int n; Data[] a = new Data[200005]; Data[] res = new Data[200005]; for (int i = 0; i \u003c a.length; i++) { a[i] = new Data(); res[i] = a[i]; } while (t-- \u003e 0) { n = in.nextInt(); for (int i = 0; i \u003c n; i++) { res[i].val = in.nextInt(); res[i].index = i; a[i] = res[i]; } Arrays.sort(a, 0, n, Comparator.comparingLong(o -\u003e o.val)); a[0].valSumLitterThanVal = a[0].val; for (int i = 1; i \u003c n; i++) { a[i].valSumLitterThanVal = a[i - 1].valSumLitterThanVal + a[i].val; } Data last = a[n - 1]; Data currentPoint; for (int i = 0; i \u003c n; i++) { currentPoint = a[i]; currentPoint.result = (i + 1 - (n - i - 1)) * currentPoint.val - currentPoint.valSumLitterThanVal + last.valSumLitterThanVal - currentPoint.valSumLitterThanVal + n; } for (int i = 0; i \u003c n; i++) { System.out.print(res[i].result); System.out.print(' '); } System.out.println(); } } private static void D() { Scanner in = new Scanner(System.in); int t = in.nextInt(); int[] a = new int[200005]; int[] res = new int[200005]; int resNum = 0; while (t-- \u003e 0) { int n = in.nextInt(); int maxD = Integer.MIN_VALUE; for (int i = 0; i \u003c n; i++) { a[i] = in.nextInt(); } for (int i = 0; i \u003c n; i++) { a[i] = a[i] - in.nextInt(); if (a[i] \u003e maxD) { maxD = a[i]; res[0] = i; resNum = 1; } else if (maxD == a[i]) { res[resNum++] = i; } } System.out.println(resNum); for (int i = 0; i \u003c resNum; i++) { System.out.print(res[i] + 1); System.out.print(' '); } System.out.println(); } } private static void C() { Scanner in = new Scanner(System.in); int t, n; int a[] = new int[1000005]; t = in.nextInt(); while (t-- \u003e 0) { n = in.nextInt(); int size = n * (n - 1) / 2; for (int i = 0; i \u003c size; i++) { a[i] = in.nextInt(); } Arrays.sort(a, 0, size); int max = a[size - 1]; for (int i = 0; i \u003c size; ) { System.out.print(a[i]); System.out.print(' '); i += --n; if (n == 0) { break; } } System.out.println(max); } } private static void B() { Scanner in = new Scanner(System.in); int t; char[] a; t = in.nextInt(); in.nextLine(); while (t-- \u003e 0) { String str = in.nextLine(); a = str.toCharArray(); int jw = -2; for (int index = a.length - 1; index \u003e= 0; index--) { if (index == jw) { if (++a[index] \u003e '9') { jw = index - 1; continue; } } char c = a[index]; if (c \u003e= '5') { jw = index - 1; } } if (jw == -2) { System.out.println(str); } else if (jw == -1) { System.out.print(1); out0(a.length); System.out.println(); } else { for (int i = 0; i \u003c= jw; i++) { System.out.print(a[i]); } out0(a.length - jw - 1); System.out.println(); } } } private static void out0(int count) { while (count-- \u003e 0) { System.out.print(0); } } private static void A() { Scanner in = new Scanner(System.in); int t, n, a; t = in.nextInt(); while (t-- \u003e 0) { n = in.nextInt(); a = 0; while (n-- \u003e 0) { a ^= in.nextInt() % 2; } System.out.println(a == 0 ? \"YES\" : \"NO\"); } } } ","permalink":"/posts/tencent-2351645/","summary":" 旧文迁移 · 补录说明\n原始发表：2023-08-10；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\nimport java.util.Arrays; import java.util.Comparator; import java.util.Scanner; public class Main { public static void main(String[] args) { G(); } static int n; static long S; static long mod = 998244353; static Edge[] a = new Edge[200005]; static int[] bcj = new int[200005]; static long[] bcjNum = new long[200005]; private static void G() { Scanner in = new Scanner(System.in); int t = in.nextInt(); for (int i = 0; i \u003c 200005; i++) { a[i] = new Edge(); } while (t-- \u003e 0) { n = in.nextInt(); S = in.nextInt(); for (int i = 0; i \u003c n - 1; i++) { a[i].u = in.nextInt() - 1; a[i].v = in.nextInt() - 1; a[i].w = in.nextInt(); bcj[i] = i; bcjNum[i] = 1; } bcj[n - 1] = n - 1; bcjNum[n - 1] = 1; Arrays.sort(a, 0, n - 1, Comparator.comparingLong(o -\u003e o.w)); long sum = 1; for (int i = 0; i \u003c n - 1; i++) { Edge edge = a[i]; if (edge.w \u003e= S) break; int uFather = getBcj(edge.u); int vFather = getBcj(edge.v); long uNum = bcjNum[uFather]; long vNum = bcjNum[vFather]; bcjNum[uFather] += bcjNum[vFather]; bcj[vFather] = uFather; long temp = uNum * vNum - 1; temp %= (mod-1); sum *= quickPow(S - edge.w + 1, temp, mod); sum %= mod; } System.out.println(sum); } } private static int getBcj(int index) { return bcj[index] == index ? index : (bcj[index] = getBcj(bcj[index])); } private static long quickPow(long a, long n, long mod) { long ans = 1; a %= mod; while (n \u003e 0) { if ((n \u0026 1) \u003e 0) {//如果n的当前末位为1 ans *= a; //ans乘上当前的a ans %= mod; } a *= a; //a自乘 a %= mod; n \u003e\u003e= 1; //n往右移一位 } return ans; } private static class Edge { int u, v, w; } private static void F() { Scanner in = new Scanner(System.in); int t = in.nextInt(); int n, q; long x, y; long[] a = new long[200005]; while (t-- \u003e 0) { n = in.nextInt(); for (int i = 0; i \u003c n; i++) { a[i] = in.nextLong(); } Arrays.sort(a, 0, n); q = in.nextInt(); while (q-- \u003e 0) { x = in.nextLong(); y = in.nextLong(); long tt = x * x - 4 * y; if (tt \u003c 0) { System.out.print(0); System.out.print(' '); continue; } long tq = sqrt(tt); long ai = x - tq; long aj = x + tq; ai /= 2; aj /= 2; if (ai + aj != x || ai * aj != y) { System.out.print(0); System.out.print(' '); continue; } long aiCount = getCount(a, n, ai); if (aiCount == 0) { System.out.print(0); System.out.print(' '); continue; } if (ai == aj) { System.out.print(aiCount * (aiCount - 1) / 2); System.out.print(' '); continue; } long ajCount = getCount(a, n, aj); System.out.print(aiCount * ajCount); System.out.print(' '); } System.out.println(); } } private static long sqrt(long n) { long low, high, mid, tmp; // 获取上下界 if (n \u003c= 1) { return n; } low = 1; high = Math.min(n, 3000000000L); // 二分法求开方 while (low \u003c= high) { mid = (low + high) / 2; tmp = mid * mid; if (tmp == n) { return mid; } else if (tmp \u003e n) { high = mid - 1; } else { low = mid + 1; } } return 0; } private static int getCount(long[] a, int n, long num) { int lIndex = getLeftIndex(a, 0, n - 1, num); int rIndex = getRightIndex(a, 0, n - 1, num); return rIndex - lIndex + 1; } private static int getLeftIndex(long[] a, int from, int end, long num) { int l = from, r = end, mid = 0; while (l \u003c= r) { mid = l + r; mid /= 2; if (a[mid] \u003c num) { l = mid + 1; } else { r = mid - 1; } } return l; } private static int getRightIndex(long[] a, int from, int end, long num) { int l = from, r = end, mid = 0; while (l \u003c= r) { mid = l + r; mid /= 2; if (a[mid] \u003c= num) { l = mid + 1; } else { r = mid - 1; } } return r; } private static class Data { long val; long valSumLitterThanVal; int index; long result; } private static void E() { Scanner in = new Scanner(System.in); int t = in.nextInt(); int n; Data[] a = new Data[200005]; Data[] res = new Data[200005]; for (int i = 0; i \u003c a.length; i++) { a[i] = new Data(); res[i] = a[i]; } while (t-- \u003e 0) { n = in.nextInt(); for (int i = 0; i \u003c n; i++) { res[i].val = in.nextInt(); res[i].index = i; a[i] = res[i]; } Arrays.sort(a, 0, n, Comparator.comparingLong(o -\u003e o.val)); a[0].valSumLitterThanVal = a[0].val; for (int i = 1; i \u003c n; i++) { a[i].valSumLitterThanVal = a[i - 1].valSumLitterThanVal + a[i].val; } Data last = a[n - 1]; Data currentPoint; for (int i = 0; i \u003c n; i++) { currentPoint = a[i]; currentPoint.result = (i + 1 - (n - i - 1)) * currentPoint.val - currentPoint.valSumLitterThanVal + last.valSumLitterThanVal - currentPoint.valSumLitterThanVal + n; } for (int i = 0; i \u003c n; i++) { System.out.print(res[i].result); System.out.print(' '); } System.out.println(); } } private static void D() { Scanner in = new Scanner(System.in); int t = in.nextInt(); int[] a = new int[200005]; int[] res = new int[200005]; int resNum = 0; while (t-- \u003e 0) { int n = in.nextInt(); int maxD = Integer.MIN_VALUE; for (int i = 0; i \u003c n; i++) { a[i] = in.nextInt(); } for (int i = 0; i \u003c n; i++) { a[i] = a[i] - in.nextInt(); if (a[i] \u003e maxD) { maxD = a[i]; res[0] = i; resNum = 1; } else if (maxD == a[i]) { res[resNum++] = i; } } System.out.println(resNum); for (int i = 0; i \u003c resNum; i++) { System.out.print(res[i] + 1); System.out.print(' '); } System.out.println(); } } private static void C() { Scanner in = new Scanner(System.in); int t, n; int a[] = new int[1000005]; t = in.nextInt(); while (t-- \u003e 0) { n = in.nextInt(); int size = n * (n - 1) / 2; for (int i = 0; i \u003c size; i++) { a[i] = in.nextInt(); } Arrays.sort(a, 0, size); int max = a[size - 1]; for (int i = 0; i \u003c size; ) { System.out.print(a[i]); System.out.print(' '); i += --n; if (n == 0) { break; } } System.out.println(max); } } private static void B() { Scanner in = new Scanner(System.in); int t; char[] a; t = in.nextInt(); in.nextLine(); while (t-- \u003e 0) { String str = in.nextLine(); a = str.toCharArray(); int jw = -2; for (int index = a.length - 1; index \u003e= 0; index--) { if (index == jw) { if (++a[index] \u003e '9') { jw = index - 1; continue; } } char c = a[index]; if (c \u003e= '5') { jw = index - 1; } } if (jw == -2) { System.out.println(str); } else if (jw == -1) { System.out.print(1); out0(a.length); System.out.println(); } else { for (int i = 0; i \u003c= jw; i++) { System.out.print(a[i]); } out0(a.length - jw - 1); System.out.println(); } } } private static void out0(int count) { while (count-- \u003e 0) { System.out.print(0); } } private static void A() { Scanner in = new Scanner(System.in); int t, n, a; t = in.nextInt(); while (t-- \u003e 0) { n = in.nextInt(); a = 0; while (n-- \u003e 0) { a ^= in.nextInt() % 2; } System.out.println(a == 0 ? \"YES\" : \"NO\"); } } } ","title":"Codeforces Round 891 (Div. 3)AC代码"},{"content":" 旧文迁移 · 补录说明\n原始发表：2023-07-31；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\nraft作为一个强一致性的集群共识算法，可以保证当集群多数节点存活时服务可用，但只能有一个领导者，有比较大的局限性。\n若是使用同集群多实例方案，所有实例同生共死，当集群非多数节点存活时，所有实例均无法对外提供服务。\n本方案提出两地四节点方案，使用在金融交易场景，在深圳与上海机房各自部署两个节点。\n方案：\n1. 我们需要在深圳与上海各放置两台服务器，设深圳两台服务器分别为A、B，上海两台服务器分别为C、D\n2. 我们将A、B、C作为一个raft集群实例的节点（深市group），D是这个集群的只读节点，并且将C的优先级设置为0，这样就可以保证在服务可用的情况下这个集群主节点一定在深市。\n3. 我们将C、D、A作为另一个raft集群实例的节点（沪市group），B是这个集群的只读节点，并且将A的优先级设置为0，这样就可以保证在服务可用的情况下这个集群主节点一定在沪市。\n4. 当深市集群可用时，A、B、C节点至少存活两个，也就意味着深市一定存活一个，由于深市节点优先级配置较高，所以可以保证主节点一定在深市。沪市同理\n5. 当任意一个节点宕机，两个集群实例存活节点数量一定不小于2，集群均可存活\n6. 当深市或沪市整个机房宕机，则本机房的集群失活，但另一个机房的集群可正常存活并对外提供服务，满足异地机房灾备\n7. 当三个节点宕机，服务不可用\n8. 特殊：由于A、B节点特殊（两个集群实例都包含），若A、B同时宕机，则服务不可用；若A、B存活而C、D宕机，两个集群均存活可用\n9. 延迟：正常情况下，A和C分别为深沪市集群的主节点，用户请求打入A/C后，均可在同机房的B/D节点返回后commit并打入交易所，异地机房延迟不影响交易延迟，满足金融低延迟需求；\n10. 注意：同集群实例内的日志有序，集群实例之间的日志不保序，即深市集群提交的日志与沪市集群提交的日志两者之间可能存在乱序，业务上需要考虑到这种情况。\n11. 注意：两个集群实例并不意味着在同一个物理节点上部署两个进程，而且同一个进程内存在两个抽象的raft节点对象，这也才能达到异地数据共享的目的（否则需要额外手段）\n12. 由于整个服务是依赖raft日志文件作为数据持久化手段，外部程序需要获取数据需要读取服务内存或者解析raft文件，耦合性较高，所以若有需要，可以另起一个E节点（数据落地节点），作为两个集群的只读节点，业务执行修改内存数据的时候，同时写入DB。外部服务需要读取数据时可直接读取DB。\n","permalink":"/posts/tencent-2351647/","summary":" 旧文迁移 · 补录说明\n原始发表：2023-07-31；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\nraft作为一个强一致性的集群共识算法，可以保证当集群多数节点存活时服务可用，但只能有一个领导者，有比较大的局限性。\n若是使用同集群多实例方案，所有实例同生共死，当集群非多数节点存活时，所有实例均无法对外提供服务。\n","title":"raft集群实战案例：两地四节点"},{"content":" 旧作补记\n原始时间：2023 年，具体月日不详\n本站补录：2026-10-12。\n翻到手机里以前写的一首词，存个档。2023 年，与她同游宝莲寺。分别时，她让我写一首诗，于是有了这首《春日宴》。\n携手同游八大殿\n拜过贤殊愿\n愿能功名吉畅\n再愿身心长健\n梁上燕儿还又眷\n会有常相见\n","permalink":"/posts/chunriyan/","summary":" 旧作补记\n原始时间：2023 年，具体月日不详\n本站补录：2026-10-12。\n翻到手机里以前写的一首词，存个档。2023 年，与她同游宝莲寺。分别时，她让我写一首诗，于是有了这首《春日宴》。\n携手同游八大殿\n拜过贤殊愿\n愿能功名吉畅\n再愿身心长健\n梁上燕儿还又眷\n会有常相见\n","title":"游宝院·春日宴"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-10-17；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n根据RAFT论文，准备自己写一个RAFT包（两手准备，有别人开源的就好了QAQ）（论文地址 https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md）\n根据论文里的内容做了接口和实体类初步设计（可能有问题，我也不知道）\n一、功能 1.选举 发起预选\n预选请求处理\n预选响应处理\n发起选举\n选举请求处理\n选举响应处理\n2.日志复制/心跳 日志推送（心跳）\n日志接收（心跳）处理\n** 3.扩容** 扩容\n减容\n二、设计 tcp传输层使用netty\n对其他节点，每个节点两个链接池，一个用来发选举信息，一个用来发日志和心跳（多连接）\n收到的信息反序列化后写入对应的disruptor队列处理\n每个节点有一个核心线程，一个核心线程池，线程用来处理选举，线程池用来处理日志和心跳相关操作\n1.Node类（物理节点） NodeChannelManage（连接管理）\nRAFT实例数组\n定时任务线程\n接口1：推送心跳 遍历当前节点所有实例（非leader不发）\n实例状态形成心跳\n心跳推送其他节点\n设置下一次心跳定时任务\n接口2：写入日志 找到对应raft实例提交\n2.NodeChannelManage类（连接管理） 选举连接\n日志连接池(一个实例哈希到一个连接，保证单实例有序)\n接口1.推送实例的心跳（心跳视为0size的日志推送） 从日志连接池推送\n接口2.推送实例的日志 从日志连接池推送\n接口3.推送实例的预选请求 从选举连接推送\n接口4.推送实例的选举请求 从选举连接推送\n接口5.推送实例的预投票请求 从选举连接推送\n接口6.推送实例的投票请求 从选举连接推送\n3.raft实例： 基本参数：\ncoreId（实例编号）\nnodeId（节点编号）\n状态：\nstate（实例状态：leader，follower，candidate，learner）\nterm（任期，第一次初始化为0）\nvotedFor（当前任期投票对象）\nleaderNodeId（领导者节点编号）\nLogMap（日志，logIndex-\u003eLog 采用内存+RocksDB,RocksDB每个coreId独立建表，日志索引从1开始）\nmaxLogIndex（已保存最大日志序号，等于logMap中最大的Index，无需落地）\n易失性状态：\ncommittedIndex（已知已提交的最高的日志条目的索引（初始值为0，单调递增））\nlastApplied（已经被应用到状态机的最高的日志条目的索引（初始值为0，单调递增））\nleaderHeartbeatTime（领导者节点最后一次发送心跳的时间，领导者自己无需维护）\n领导人专属易失性状态：\nnextIndex[] 对于每一台服务器，发送到该服务器的下一个日志条目的索引（初始值为领导人最后的日志条目的索引+1）\nmatchIndex[] 对于每一台服务器，已知的已经复制到该服务器的最高日志条目的索引（初始值为0，单调递增）\nheartbeatTime[] 对于每一台服务器，最后一次心跳响应的时间戳\nlastVoteTime 最后一次投票时间戳（包括预投票）\n选举线程（状态检查，leader心跳回调处理）\n日志线程（接收日志，心跳的处理）\n日志推送线程（leader专用，其他节点用不到）\n日志apply线程\n初始化\nterm=Index最大的日志的term\nmaxLogInde=日志最大Index\nstate=跟随者\n设置状态检查定时任务（随机延迟1~2倍）\n接口1.写入日志 submit if 不是领导\nreturn\n提交日志推送线程\n接口2.接收日志/心跳 appendEntries 日志提交 日志线程\nif（日志的任期小于本节点任期）return\n更新leaderHeartbeatTime\nif（本节点状态为候选人）\n调用投票结果处理（失败）\nif（日志的 prevLogIndex 且 prevLogTerm在本实例找不到一个日志能匹配上）推送失败响应并返回\nif（日志的Index在本实例存在，且term不一致）删除index以及之后的所有日志\n追加未保存日志\nif 已提交日志Index大于本节点已提交日志Index：\n更新本实例committedIndex=min（日志最大序号，新日志的committedIndex更新）\n更新lastApplied+1 循环到本实例committedIndex\nlastApplied日志提交日志apply线程对状态机进行apply\n推送成功并返回\n接口3.接收日志/心跳的回调 onAppendEntries（发起者的回调）（term，succeed，logIndex（若succeed为false使用）） （在选举线程里）\nterm小于本实例term直接丢弃（可能是之前任期的leader的回调，不应理会）\n更新heartbeatTime[]\nif 失败：\nnextIndex[]中 该接收者的nextIndex–（需要优化的话让跟随者返回可能正常的logIndex（跟随者冲突条目的term的相同日志全部越过，返回小于冲突条目term的最大logIndex，这里直接赋值））；\n对该节点推送日志（一条）\nif 成功：\nnextIndex[]中 该接收者的nextIndex=请求prevLogIndex+请求日志size+1\nmatchIndex[]中 该接收者的nextIndex=请求prevLogIndex+请求日志size\nif(matchIndex[]中 第N/2+1大的数字\u003e当前committedIndex)\ncommittedIndex=matchIndex[]中 第N/2+1大的数字\n对所有节点发起心跳\nif（nextIndex[]中 该接收者的nextIndex\u003c=当前日志序号）\n对该节点推送新日志（记得用批大小控制每次发送数量，记得和matchIndex比对，不要发送太多在途日志）\n接口4.定时检查状态 （在选举线程里）\nswitch 本实例状态：\n是领导：检查心跳时间戳是否有过半超时，若过半超时则下台\n是跟随者：检查最后一次领导者心跳时间戳是否超时，若超时且最后一次投票的时间戳也超时，则发起预投票\n重新定时检查状态任务提交定时任务（延时：领导者设定为超时时间，跟随者设置超时时间~超时时间*2之间的随机数）\n接口 5.推送日志（nodeId，size）（newLog） if 本节点非Leader 返回\n提交日志推送线程\nif 是新日志推送\n初始化日志\nappendEntries（本地）\nappendEntries（推送所有节点（nextId匹配不上的不推））\nelse 按照nodeId和size推送现有日志\n接口 6.发起预投票 （在选举线程里）\nlastVoteTime更新为当前时间\n初始化preVoteSet并向所有节点推送预投票请求\n接口7.预投票请求处理（请求体：term，candidateId，lastLogIndex，lastLogTerm） （在选举线程里）\nif term小于本实例term 返回false\nif 当前leader的心跳未超时 推送响应false\nif votedFor为空或等于candidateId，并且lastLogIndex\u003e=自己的maxLogIndex\n更新lastVoteTime，推送响应true\npreVoteSet\n接口8.预投票响应处理（响应体：term，voteGranted） （在选举线程里）\n校验任期，若不相等则丢弃\nif voteGranted\npreVoteSet.add（响应节点NodeId）\nif preVoteSet.size\u003e节点数量/2\n预投票结果处理（成功）\n接口9.预投票结果处理 （在选举线程里）\n校验任期，若不相等则丢弃\nif 失败：无事发生\nif 成功：发起正式投票\n接口10.发起投票 （在选举线程里）\n状态变成候选人\nterm++\nvoteFor改为自己\n设置候选定时任务（超时未成功则失败，记得设随机）\nlastVoteTime更新为当前时间\n初始化voteSet并向所有节点发送投票信息\n接口11.投票请求处理（请求体：term，candidateId，lastLogIndex，lastLogTerm） （在选举线程里）\nif term小于本实例term 返回false\nif term 大于本实例term 或者votedFor为空或等于candidateId，并且lastLogIndex\u003e=自己的maxLogIndex\n更新lastVoteTime、term、votedFor，返回true\nvoteSet\n接口12.投票响应处理（响应体：term，voteGranted） （在选举线程里）\n校验任期，若不相等则丢弃\nif voteGranted\nVoteSet.add（响应节点NodeId）\nif VoteSet.size\u003e节点数量/2\n投票结果处理（成功）\n接口13.投票结果处理（term，result） （在选举线程里）\n校验任期，若不相等则丢弃\nif 超时失败\n退回跟随者状态，重新发起预投票\nif 选举失败（收到新leader心跳）\n退回跟随者状态\nif 成功\n当选leader\n4.LogEntry类（物理日志（心跳）实例） Int coreId 实例Id\nlong logTerm 本日志任期\nlong leaderId 领导人Id\nlong prevLogTerm 紧邻新日志条目之前的那个日志条目的任期\nlong prevLogIndex 紧邻新日志条目之前的那个日志条目的索引\nlong leaderCommit 领导人已知已提交日志的Index\nint size 本次日志数量（心跳填0）\nsize个：\nint length 单条日志长度\nbyte[] data 单条日志数据\n","permalink":"/posts/tencent-2351648/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-10-17；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n根据RAFT论文，准备自己写一个RAFT包（两手准备，有别人开源的就好了QAQ）（论文地址 https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md）\n","title":"RAFT代码设计"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-09-16；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n前言 本文参考了 Java-JSR-269-插入式注解处理器 ，笔者也是根据该文入门，感谢大佬。\n本文是Java字节码深挖系列最后一章，被上海的疫情以及塔某些不可描述的操作搞的没心情写文，所以鸽了这么久~\n我用这个东西做了个序列化框架，性能比protobuf强20%左右，易用性比protobuf高很多（传输类打个注解就完事了，写什么乱七八糟的文件），原理很简单，想看的直接拉到尾部。\n编译原理 前面讲到的Javassist，cglib等等，虽然确实操作了字节码，但还是运行时生成，未涉及编译的过程，而JSR-269标准允许我们在编译过程中对字节码进行操作。如果学过编译原理的都知道，编译的过程一般包括：词法分析，语法分析，语义分析，中间代码生成，中间代码优化，目标代码生成，目标代码优化。而具体到Java语言，Javac的编译过程是：准备环境，词法分析，语法分析，填充符号表，注解处理，语义分析，标记检查，数据流分析，解语法糖，字节码生成\n在这个过程中，我们着重关注注解处理这一步，java源代码经过词法分析和语法分析之后会生成抽象语法树，编译器后续的操作都在该语法树上进行，不会再对源代码（字符串）进行操作。注解处理这一步也是在抽象语法树上进行的，填充符号表过程的出口是一个待处理列表（To Do List），包含了每一个编译单元的抽象语法树的顶级节点。完成抽象语法树的构建并填充符号表之后，编译器将会开始进行注解处理，jdk1.6实现了JSR-269规范，并提供了标准的Pluggable Annotations Processing API（插入式注解处理API），我们根据这套API，就可以进行插入式注解处理器的开发了，我们可以把注解器简单的理解为编译器的插件。\n现在，我们可以干涉java编译器的编译过程了，抽象语法树中包含了原始代码的一切（甚至包括代码注释），而我们通过这套API又可以修改语法树所以只要有足够的创意，我们可以做非常多的事情。\n开发流程 我们开发插入式注解器可以这么做，首先编写注解器代码，注解器类上应该带上@SupportedSourceVersion和@SupportedAnnotationTypes注解表示该注解器作用于哪个版本的java源码，以及作用于哪个注解。注解器还应该继承AbstractProcessor类接口，并重写public boolean process(Set\u003c? extends TypeElement\u003e annotations, RoundEnvironment roundEnv)方法，在该方法中使用访问者模式对语法树进行调整。编写完成后，我们应该编写配置文件，当然，谷歌的com.google.auto.service.AutoService注解可以帮助我们不用编写该文件（直接将该注解写在注解器上即可），而后编译打包注解器。之后，我们就可以在新的代码项目中使用该注解器，只需要引入注解器所在的包后进行编译即可（笔者工作中使用的是maven，可以傻瓜式调用，甚至可以对注解器进行debug）。我们可以使用mvnDebug命令对注解器进行bug，debug具体使用方式这里不展开了。\n接下来说说注解器的一些api，抄自 Java-JSR-269-插入式注解处理器 ：\n3 编译相关的数据结构与API 3.1 JCTree JCTree是语法树元素的基类，包含一个重要的字段pos，该字段用于指明当前语法树节点（JCTree）在语法树中的位置，因此我们不能直接用new关键字来创建语法树节点，即使创建了也没有意义。此外，结合访问者模式，将数据结构与数据的处理进行解耦，部分源码如下：\npublic abstract class JCTree implements Tree, Cloneable, DiagnosticPosition { public int pos = -1; ... public abstract void accept(JCTree.Visitor visitor); ... } 这里重点介绍几个JCTree的子类，下面的Demo会用到\nJCStatement：声明语法树节点，常见的子类如下 JCBlock：语句块语法树节点\nJCReturn：return语句语法树节点\nJCClassDecl：类定义语法树节点\nJCVariableDecl：字段/变量定义语法树节点\nJCMethodDecl：方法定义语法树节点\nJCModifiers：访问标志语法树节点\nJCExpression：表达式语法树节点，常见的子类如下\nJCAssign：赋值语句语法树节点\nJCIdent：标识符语法树节点，可以是变量，类型，关键字等等\n3.2 TreeMaker TreeMaker用于创建一系列语法树节点，创建时会为创建出来的JCTree设置pos字段，所以必须用上下文相关的TreeMaker对象来创建语法树节点，而不能直接new语法树节点\n源码可以参考TreeMaker DOC\n3.2.1 TreeMaker.Modifiers TreeMaker.Modifiers方法用于创建访问标志语法树节点(JCModifiers)，源码如下：\nflags：访问标志\nannotations：注解列表\npublic JCModifiers Modifiers(long flags) { return Modifiers(flags, List.\u003c JCAnnotation \u003enil()); } public JCModifiers Modifiers(long flags, List\u003cJCAnnotation\u003e annotations) { JCModifiers tree = new JCModifiers(flags, annotations); boolean noFlags = (flags \u0026 (Flags.ModifierFlags | Flags.ANNOTATION)) == 0; tree.pos = (noFlags \u0026\u0026 annotations.isEmpty()) ? Position.NOPOS : pos; return tree; } 其中入参flags可以用枚举类型com.sun.tools.javac.code.Flags，且支持拼接（枚举值经过精心设计）\n例如，我们可以这样用\ntreeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL); 3.2.2 TreeMaker.ClassDef TreeMaker.ClassDef用于创建类定义语法树节点(JCClassDecl)，源码如下：\nmods：访问标志\nname：类名\ntyparams：泛型参数列表\nextending：父类\nimplementing：接口列表\ndefs：类定义的详细语句，包括字段，方法定义等等\npublic JCClassDecl ClassDef(JCModifiers mods, Name name, List\u003cJCTypeParameter\u003e typarams, JCExpression extending, List\u003cJCExpression\u003e implementing, List\u003cJCTree\u003e defs) { JCClassDecl tree = new JCClassDecl(mods, name, typarams, extending, implementing, defs, null); tree.pos = pos; return tree; } 3.2.3 TreeMaker.MethodDef TreeMaker.MethodDef用于创建方法定义语法树节点（JCMethodDecl），源码如下：\nmods：访问标志\nname：方法名\nrestype：返回类型\ntyparams：泛型参数列表\nparams：参数列表\nthrown：异常声明列表\nbody：方法体\ndefaultValue：默认方法（可能是interface中的那个default）\nm：方法符号\nmtype：方法类型。包含多种类型，泛型参数类型、方法参数类型，异常参数类型、返回参数类型\npublic JCMethodDecl MethodDef(JCModifiers mods, Name name, JCExpression restype, List\u003cJCTypeParameter\u003e typarams, List\u003cJCVariableDecl\u003e params, List\u003cJCExpression\u003e thrown, JCBlock body, JCExpression defaultValue) { JCMethodDecl tree = new JCMethodDecl(mods, name, restype, typarams, params, thrown, body, defaultValue, null); tree.pos = pos; return tree; } public JCMethodDecl MethodDef(MethodSymbol m, Type mtype, JCBlock body) { return (JCMethodDecl) new JCMethodDecl( Modifiers(m.flags(), Annotations(m.getAnnotationMirrors())), m.name, Type(mtype.getReturnType()), TypeParams(mtype.getTypeArguments()), Params(mtype.getParameterTypes(), m), Types(mtype.getThrownTypes()), body, null, m).setPos(pos).setType(mtype); } 其中，返回类型填null或者treeMaker.TypeIdent(TypeTag.VOID)都代表返回void类型\n3.2.4 TreeMaker.VarDef TreeMaker.VarDef用于创建字段/变量定义语法树节点（JCVariableDecl），源码如下：\nmods：访问标志\nvartype：类型\ninit：初始化语句\nv：变量符号\npublic JCVariableDecl VarDef(JCModifiers mods, Name name, JCExpression vartype, JCExpression init) { JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null); tree.pos = pos; return tree; } public JCVariableDecl VarDef(VarSymbol v, JCExpression init) { return (JCVariableDecl) new JCVariableDecl( Modifiers(v.flags(), Annotations(v.getAnnotationMirrors())), v.name, Type(v.type), init, v).setPos(pos).setType(v.type); } 3.2.5 TreeMaker.Ident\nTreeMaker.Ident用于创建标识符语法树节点（JCIdent），源码如下：\npublic JCIdent Ident(Name name) { JCIdent tree = new JCIdent(name, null); tree.pos = pos; return tree; } public JCIdent Ident(Symbol sym) { return (JCIdent)new JCIdent((sym.name != names.empty) ? sym.name : sym.flatName(), sym) .setPos(pos) .setType(sym.type); } public JCExpression Ident(JCVariableDecl param) { return Ident(param.sym); } 3.2.6 TreeMaker.Return TreeMaker.Return用于创建return语句语法树节点（JCReturn），源码如下：\npublic JCReturn Return(JCExpression expr) { JCReturn tree = new JCReturn(expr); tree.pos = pos; return tree; } 3.2.7 TreeMaker.Select TreeMaker.Select用于创建域访问/方法访问（这里的方法访问只是取到名字，方法的调用需要用TreeMaker.Apply）语法树节点（JCFieldAccess），源码如下：\nselected：.运算符左边的表达式\nselector：.运算符右边的名字\npublic JCFieldAccess Select(JCExpression selected, Name selector) { JCFieldAccess tree = new JCFieldAccess(selected, selector, null); tree.pos = pos; return tree; } public JCExpression Select(JCExpression base, Symbol sym) { return new JCFieldAccess(base, sym.name, sym).setPos(pos).setType(sym.type); } 3.2.8 TreeMaker.NewClass TreeMaker.NewClass用于创建new语句语法树节点（JCNewClass），源码如下：\nencl：不太明白此参数含义\ntypeargs：参数类型列表\nclazz：待创建对象的类型\nargs：参数列表\ndef：类定义\npublic JCNewClass NewClass(JCExpression encl, List\u003cJCExpression\u003e typeargs, JCExpression clazz, List\u003cJCExpression\u003e args, JCClassDecl def) { JCNewClass tree = new JCNewClass(encl, typeargs, clazz, args, def); tree.pos = pos; return tree; } 3.2.9 TreeMaker.Apply TreeMaker.Apply用于创建方法调用语法树节点（JCMethodInvocation），源码如下：\ntypeargs：参数类型列表\nfn：调用语句\nargs：参数列表\npublic JCMethodInvocation Apply(List\u003cJCExpression\u003e typeargs, JCExpression fn, List\u003cJCExpression\u003e args) { JCMethodInvocation tree = new JCMethodInvocation(typeargs, fn, args); tree.pos = pos; return tree; } 3.2.10 TreeMaker.Assign TreeMaker.Assign用于创建赋值语句语法树节点（JCAssign），源码如下：\nlhs：赋值语句左边表达式\nrhs：赋值语句右边表达式\npublic JCAssign Assign(JCExpression lhs, JCExpression rhs) { JCAssign tree = new JCAssign(lhs, rhs); tree.pos = pos; return tree; } 3.2.11 TreeMaker.Exec TreeMaker.Exec用于创建可执行语句语法树节点（JCExpressionStatement），源码如下：\npublic JCExpressionStatement Exec(JCExpression expr) { JCExpressionStatement tree = new JCExpressionStatement(expr); tree.pos = pos; return tree; } 例如，TreeMaker.Apply以及TreeMaker.Assign就需要外面包一层TreeMaker.Exec来获得一个JCExpressionStatement\n3.2.12 TreeMaker.Block TreeMaker.Block用于创建组合语句语法树节点（JCBlock），源码如下：\nflags：访问标志\nstats：语句列表\npublic JCBlock Block(long flags, List\u003cJCStatement\u003e stats) { JCBlock tree = new JCBlock(flags, stats); tree.pos = pos; return tree; } 3.3 com.sun.tools.javac.util.List 上述JSR-269 API中会涉及到一个List，这个List不是java.util.List，它是com.sun.tools.javac.util.List，这个List的操作比较奇特，不支持链式操作。下面给出部分源码，List包含两个字段，head和tail，其中head只是一个节点，而tail是一个List\npublic class List\u003cA\u003e extends AbstractCollection\u003cA\u003e implements java.util.List\u003cA\u003e { public A head; public List\u003cA\u003e tail; private static final List\u003c?\u003e EMPTY_LIST = new List\u003cObject\u003e((Object)null, (List)null) { public List\u003cObject\u003e setTail(List\u003cObject\u003e var1) { throw new UnsupportedOperationException(); } public boolean isEmpty() { return true; } }; List(A head, List\u003cA\u003e tail) { this.tail = tail; this.head = head; } public static \u003cA\u003e List\u003cA\u003e nil() { return EMPTY_LIST; } public List\u003cA\u003e prepend(A var1) { return new List(var1, this); } public List\u003cA\u003e append(A var1) { return of(var1).prependList(this); } public static \u003cA\u003e List\u003cA\u003e of(A var0) { return new List(var0, nil()); } public static \u003cA\u003e List\u003cA\u003e of(A var0, A var1) { return new List(var0, of(var1)); } public static \u003cA\u003e List\u003cA\u003e of(A var0, A var1, A var2) { return new List(var0, of(var1, var2)); } public static \u003cA\u003e List\u003cA\u003e of(A var0, A var1, A var2, A... var3) { return new List(var0, new List(var1, new List(var2, from(var3)))); } ... } 3.4 com.sun.tools.javac.util.ListBuffer 由于com.sun.tools.javac.util.List用起来不是很方便，而ListBuffer的行为与java.util.List的行为类似，并且提供了转换成com.sun.tools.javac.util.List的方法\nListBuffer\u003cJCTree.JCStatement\u003e jcStatements = new ListBuffer\u003c\u003e(); //添加语句 \" this.xxx = xxx; \" jcStatements.append(...); //添加Builder模式中的返回语句 \" return this; \" jcStatements.append(...); List\u003cJCTree.JCStatement\u003e lst = jcStatements.toList(); 3.5 tricky 3.5.1 创建一个构造方法 注意点：方法的名字就是\u003cinit\u003e\ntreeMaker.MethodDef( treeMaker.Modifiers(Flags.PUBLIC), //访问标志 names.fromString(\"\u003cinit\u003e\"), //名字 treeMaker.TypeIdent(TypeTag.VOID), //返回类型 List.nil(), //泛型形参列表 List.nil(), //参数列表 List.nil(), //异常列表 jcBlock, //方法体 null //默认方法（可能是interface中的那个default） ); 3.5.2 创建一个方法的参数 注意点：访问标志设置成Flags.PARAMETER\ntreeMaker.VarDef( treeMaker.Modifiers(Flags.PARAMETER), //访问标志。极其坑爹！！！ prototypeJCVariable.name, //名字 prototypeJCVariable.vartype, //类型 null //初始化语句 ); 3.5.3 创建一条赋值语句 treeMaker.Exec( treeMaker.Assign( treeMaker.Select( treeMaker.Ident(names.fromString(THIS)), jcVariable.name ), treeMaker.Ident(jcVariable.name) ) ) 3.5.4 创建一条new语句 treeMaker.NewClass( null, //尚不清楚含义 List.nil(), //泛型参数列表 treeMaker.Ident(builderClassName), //创建的类名 List.nil(), //参数列表 null //类定义，估计是用于创建匿名内部类 ) 3.5.5 创建一条方法调用语句 treeMaker.Exec( treeMaker.Apply( List.nil(), treeMaker.Select( treeMaker.Ident(getNameFromString(IDENTIFIER_DATA)), jcMethodDecl.getName() ), List.of(treeMaker.Ident(jcVariableDecl.getName())) //传入的参数集合 ) ) 提示：我们开发后使用的时候，编译可能会报一些错误（我忘记了），比如树节点pos不对之类的，这是因为treeMaker默认的pos是-1，我们只需要在treeMaker使用之前加入如下代码即可： this.treeMaker.at(jcClass.pos); 工程应用-高性能序列化框架（只讲思路） 我们先来想一想，什么样的序列化代码性能最高？比如我们要传输int a=1,boolean b=false,long c=10,String d=“123”;这些数据\n把他们序列化成二进制流最高性能的方案是不是类似这种写法\nByteBuffer byteBuffer=ByteBuffer.allocate(13) .order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putInt(a); byteBuffer.put(b); byteBuffer.putLong(c); return byteBuffer.array(); 那如果有一百个接口，每个接口有一百个字段呢？一个个写可能要熬出一个大秃头。\n现在我们有了JSR-269，我们完全可以把这个事情交给编译器去做，在需要序列化的类上加上注解，然后让注解器去生成该类的序列化和反序列化代码。\n对于父类的序列化支持也很简单，在需要支持父类的序列化函数第一行将super的该方法写入即可。\n同理，对复杂对象的序列化（成员变量也为对象）也可以在该行调用改成员变量的序列化函数，（注：为了节约空间和性能，我们应该将buffer对象传入函数对buffer对象读写，而不是将该对象序列化好的字节数组返回后再写入buffer对象）。\n以上方案对于有递归思维的程序员应该很好理解。\n对于泛型的支持则稍微困难了一些，我们都知道java编译时候的泛型擦除是一个假擦除，实际上还是在类的字节码中，擦了个寂寞，所以我们反射的时候实际上是可以获取父类的泛型信息–clz.getGenericSuperclass().getActualTypeArguments()[index]。\n但是我们编译的时候跨语法树去另一个类中获取这些信息还是比较麻烦的，所以这步我使用了**反射+缓存（当前类+父类+泛型下标作为key）**进行处理，实测对性能几乎没有影响。\n不过对于当前类的泛型支持目前还做不太到，有思路的小伙伴可以一起交流一下~。\n目前测试下来性能大约超过protobuf 20% ，2.0将会结合（绑定）netty的零拷贝技术，这样在netty的使用场景下性能应该还能再进一步提高（提高很多，不论是空间占用还是耗时）。\n当然，这样还有一个小问题，就是idea无法识别你在编译后才会出现的函数，所以调用的时候会爆红，编译倒是正常。对于这个问题，我模仿lombok写了一个idea的插件，使用该插件后idea就可以正确识别相关函数了。关于idea插件的开发，有空了我专门找个时间写一篇~\n","permalink":"/posts/tencent-2351649/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-09-16；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n前言 本文参考了 Java-JSR-269-插入式注解处理器 ，笔者也是根据该文入门，感谢大佬。\n本文是Java字节码深挖系列最后一章，被上海的疫情以及塔某些不可描述的操作搞的没心情写文，所以鸽了这么久~\n","title":"Java字节码深挖 第四站：JSR-269"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-09-03；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\nPaxos算法是一种一致性算法，用于在一个分布式多节点系统中确定一个确认的值，这个值可以是一条日志，可以是选举领导者，也可以是自己定义的任意数据。\n在paxos中，存在三种角色：提议者、仲裁者、学习者。每个节点可以身兼数职，但至少是其中一个。\nPaxos确定的值是一组，曾经决策确定过的值，一个值在当次仲裁中被选定，那么便可以保证这个值不会丢失。\n在每次仲裁过程中，提议者负责提出提案，仲裁者负责仲裁，学习者将最终仲裁的值学习。\nPaxos的约束条件如下：\nP1：一个Acceptor必须接受它收到的第一个提案。\nP1a：一个Acceptor只要尚未响应过任何编号大于N的Prepare请求，那么他就可以接受这个编号为N的提案。\nP2：如果某个value为v的提案被选定了，那么每个编号更高的被选定提案的value必须也是v。\nP2a：如果某个value为v的提案被选定了，那么每个编号更高的被Acceptor接受的提案的value必须也是v。\nP2b：如果某个value为v的提案被选定了，那么之后任何Proposer提出的编号更高的提案的value必须也是v。\nP2c：对于任意的N和V，如果提案[N, V]被提出，那么存在一个半数以上的Acceptor组成的集合S，满足以下两个条件中的任意一个：\nS中每个Acceptor都没有接受过编号小于N的提案。\nS中Acceptor接受过的最大编号的提案的value为V。\n具体流程：\n提议者需要维护本次仲裁中，本节点的提案轮次N。\n仲裁者会维护2个数据：当前最大轮次N，当前本节点推选提案。\n提议者提议分为两个阶段，准备阶段和提议阶段。\n准备阶段：提议者向所有仲裁者发送准备请求，包含自己本次提议的轮次N。\n仲裁者接到准备请求后，将请求中的N与仲裁节点中维护的最大轮次N对比，若请求中的N小于或等于节点N则忽略，否则维护节点N，并发送响应，响应包含原节点N和原提案。\n提议者收到半数以上仲裁者响应后，开始提议阶段。\n提议阶段：仲裁者响应中若包含已选定提案，则提议阶段使用该提案进行提议，否则选择响应中N最大的提案，若响应中不包含提案，则提议者自己决定提案。\n学习者则不断获取每个仲裁者的仲裁情况，当得到某一轮次的仲裁者超过半数仲裁了同一个值，则学习者确认该值为本次仲裁的最终值。\n个人理解：\n提议者在提议阶段时候，使用了自己的轮次N对半数以上的仲裁者询问，在本次交互中，提议者相对于锁住了轮次N的提议，也就是说当提议者提议的时候，这一轮次只有这个提议者可以提议，因为超过半数的仲裁者回应代表着其他提议者一定拿不到半数以上的轮次N回应。\n提议者提议阶段，仲裁者检查轮次是否匹配，匹配则接收，否则丢弃。丢弃出现的时候必然是因为有更高的轮次被锁住，即轮次N被抢占了。\n不可能存在N轮次超过一半仲裁者仲裁的值与N+1轮次不同（比如5个节点，3个节点在轮次2仲裁A值，1个节点在轮次3选择B值），理由如下：若要达成当前局面，首先有提议节点的N+1轮次在准备阶段结束时必须获取超过一半的响应，并且响应中不存在N轮次的值（否则该节点的N+1轮次提出的值必然是N轮次的值），由此可推出超过一半仲裁者在N+1轮次的准备请求到达前必然没有收到N轮次的提议，又因为N+1轮次的准备请求会将N轮次的提议请求无效化（锁抢占），即N轮次提议也不能在N+1轮次响应后被接受，出现矛盾，论证成立。\n由3递推可得将结论扩展到N与N+k的场景，也就是定律P2。\n该算法与活锁的思路类似，经过推理后发现该算法也存在活锁可能存在的问题：即提议者在准备阶段互相抢占其他提议者的轮次锁，导致轮次不断增加却没有提议者能够进入提议阶段。这种问题的解决方案可以参考RAFT的随机延迟方法，可以简单高效的将概率降低。\n","permalink":"/posts/tencent-2351650/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-09-03；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\nPaxos算法是一种一致性算法，用于在一个分布式多节点系统中确定一个确认的值，这个值可以是一条日志，可以是选举领导者，也可以是自己定义的任意数据。\n","title":"分布式一致性算法-关于Paxos算法的理解"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-08-16；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n基本算法-令牌桶 原始算法 令牌桶（Token-Bucket）是目前最常采用的一种流量测量方法，我们可以想象一个存放令牌的容器，预先设定一定的容量。系统按设定的速度向桶中放置令牌，当桶中令牌满时，多余的令牌将被丢弃，当请求流量进入服务时，需要从桶内获取令牌才可被服务处理，否则将执行拒绝策略。\n优化 我们可以将该算法优化，记录下最后一次获取令牌的时间戳和令牌数量，在下一次取令牌的时候根据两次时间戳的间隔计算出应该维护的令牌数后再取令牌，算法时间复杂度和空间复杂度均为O(1)，且不需要额外线程开销。\n货币化改造 对于不同的接口，我们可以配置不同的消耗，对于轻量接口我们可以配置较少令牌数量，对于较重接口我们可以配置较多令牌数量，比如对于A接口系统满载可以承受1000TPS，而对于B接口系统满载可以承受2000TPS，那么只需要将A与B接口消耗的令牌数比例设为2:1即可在一定程度上更充分利用系统的性能。\n同时，我们新增了窃取令牌数接口和补偿令牌数接口，其中窃取接口可以强行减去当前桶的令牌数量（可将当前桶的数量扣为负数，但是不小于负的桶容量，区别于普通的流控接口），补偿令牌数接口可以强行增加当前桶的令牌数（不得大于桶容量）\n算法性能 加锁后每次调用大约需要花费40纳秒（多线程涉及锁竞争，可能会导致延迟增大，但对于大部分服务可以忽略不计）。\n分级流控算法 原理 在货币化改造的基础上，我们还可以对不同的功能接口设置不同的等级，同时将设置多个不同等级的令牌桶，不同等级的接口应该在不同等级的令牌桶中获取执行令牌，当获取成功后，我们将所有低于该等级的令牌桶的令牌窃取相应的数量（为保证模型简单，该数量与该接口设置的消耗数量一致）。\n服务降级 当高级接口调用频繁的时候，低级令牌桶的令牌总是会被窃取走（因为窃取可以窃取为负数，而流控接口若扣除失败则不会扣除任何令牌，可以理解为窃取优先级高于流控），低级接口获取不到令牌，只能执行事先设定的拒绝策略，从而达到服务降级逻辑\n多节点同步 我们可以通过一定的同步机制将节点的消耗传递给其他节点，并在其他节点窃取令牌以达到分布式全局流控的效果。\n","permalink":"/posts/tencent-2351651/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-08-16；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n基本算法-令牌桶 原始算法 令牌桶（Token-Bucket）是目前最常采用的一种流量测量方法，我们可以想象一个存放令牌的容器，预先设定一定的容量。系统按设定的速度向桶中放置令牌，当桶中令牌满时，多余的令牌将被丢弃，当请求流量进入服务时，需要从桶内获取令牌才可被服务处理，否则将执行拒绝策略。\n","title":"令牌桶算法和分布式集群分级流控（服务降级）"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-05-05；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n不同于jdk自带的动态代理只能针对接口生成代理类，cglib可以对类实现代理，实现也非常的傻瓜式，我们只需要实现net.sf.cglib.proxy.MethodInterceptor接口，使用方法与jdk自带的动态代理类似。\nnet.sf.cglib.proxy.MethodInterceptor接口中只有一个待实现的方法：\npublic Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable; 其中第一个参数便是被调用的对象本身，第二个参数是被调用的方法，第三个参数是被调用的参数，第四个参数是被代理的方法，其中第一个和第三个参数比较好理解，method和proxy可能造成歧义，我这里解释一下，method是当前对象的方法，调用这个方法会再次进入intercept回调，而proxyMethod是被代理的方法，调用proxyMethod.invokeSuper(Object obj, Object[] args)方法即可调用原始的方法。\n实现了本接口后，我们便可以使用 Enhancer类生成clz类增强后的代理实例了\nEnhancer enhancer = new Enhancer(); //设置代理目标 enhancer.setSuperclass(clz); //设置单一回调对象，在调用中拦截对目标方法的调用 enhancer.setCallback(methodInterceptor); //设置类加载器 enhancer.setClassLoader(clz.getClassLoader()); Object obj=enhancer.create(); 关于应用，我使用cglib实现了参数校验框架，实现逻辑如下：\n1.我们可以设置校验规则注解，将其注在需要被校验的成员变量上，该注解内部需要实现Validate接口方法，并将该类设为默认validate值。\n2.递归检查注解@RecursionCheck，将其注在同样需要校验的对象成员变量上。\n3.关联注解@Relevancy(String[])，将其注在可能改变成员变量值的方法上，注解参数为字符串数组，需要把可能被改变值的成员变量名称写入。\n核心实现逻辑如下：\n1.net.sf.cglib.proxy.MethodInterceptor接口实现：\npublic class ValCheckInterceptor implements MethodInterceptor { public static MethodInterceptor I = new ValCheckInterceptor(); private static final Map\u003cClass\u003c?\u003e, Map\u003cMethod, Set\u003cField\u003e\u003e\u003e classMap = new ConcurrentHashMap\u003c\u003e(); @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Class\u003c?\u003e clz = o.getClass().getSuperclass(); classMap.computeIfAbsent(clz, this::initClassCheckMethod); Object result = methodProxy.invokeSuper(o, objects); Map\u003cMethod, Set\u003cField\u003e\u003e methodFieldMap = classMap.get(o.getClass().getSuperclass()); if (methodFieldMap != null \u0026\u0026 methodFieldMap.containsKey(method)) { Set\u003cField\u003e fields = methodFieldMap.get(method); for (Field field : fields) { field.setAccessible(true); for (Annotation annotation : field.getAnnotations()) { if (annotation instanceof RecursionCheck) { Object f = field.get(o); if (f != null \u0026\u0026 f.getClass().equals(field.getType())) { Object feh = ValCheckProxyFactory.getProxyInstance(f, this); field.set(o, feh); } continue; } Class\u003c? extends Validate\u003e validateClz = null; try { Method validateCMethod = annotation.getClass().getMethod(\"validate\"); validateClz = (Class\u003c? extends Validate\u003e) validateCMethod.invoke(annotation); } catch (Throwable ignored) { } if (validateClz != null) { Validate validate = validateClz.newInstance(); Object value = field.get(o); validate.validate(o, field, value, annotation); } } } } return result; } public Map\u003cMethod, Set\u003cField\u003e\u003e initClassCheckMethod(Class\u003c?\u003e clz) { Map\u003cMethod, Set\u003cField\u003e\u003e methodFieldMap = new ConcurrentHashMap\u003c\u003e(); try { Set\u003cField\u003e fields = ReflectUtil.getFields(clz); for (Field field : fields) { PropertyDescriptor pd = null; pd = new PropertyDescriptor(field.getName(), clz); Method setMethod = pd.getWriteMethod();//获得set方法 methodFieldMap.computeIfAbsent(setMethod, m -\u003e { Set\u003cField\u003e set = ConcurrentHashMap.newKeySet(); set.add(field); return set; }); } } catch (IntrospectionException e) { e.printStackTrace(); } //获取对象的关联注解方法 Arrays.stream(clz.getMethods()).forEach(method1 -\u003e { Relevancy relevancy = method1.getAnnotation(Relevancy.class); if (relevancy == null) return; methodFieldMap.computeIfAbsent(method1, m -\u003e ConcurrentHashMap.newKeySet()); Set\u003cField\u003e fields = methodFieldMap.get(method1); String[] fstr = relevancy.value(); for (String s : fstr) { fields.add(ReflectUtil.getField(clz, s)); } }); return methodFieldMap; } } 样例如下\n@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Max { double value(); String message() default \"\"; Class\u003c? extends Validate\u003cMax\u003e\u003e validate() default Max.DefaultValidate.class; class DefaultValidate implements Validate\u003cMax\u003e { @Override public void validate(Object obj, Field field, Object value, Max annotation) throws ValidateException { if (!(value instanceof Number)) { return; } double v; if (value instanceof BigDecimal) { v = ((BigDecimal) value).doubleValue(); } else { v = (double) value; } if (v \u003e annotation.value()) { throw new ValidateException(obj, field, value, annotation, \"value should \u003c= \" + annotation.value(), annotation.message()); } } } } public class TestModel extends BaseModel{ /** * */ @Max(100) private Integer integer; @Max(100) private Integer integer2; @RecursionCheck private TestModel2 m2; public TestModel2 getM2() { return m2; } public void setM2(TestModel2 m2) { this.m2 = m2; } public static class TestModel2 extends BaseModel{ @ValMax(100) private Integer integer1; public Integer getInteger1() { return integer1; } public void setInteger1(Integer integer1) { this.integer1 = integer1; } } public Integer getInteger() { return integer; } public Integer getInteger2() { return integer2; } public void setInteger2(Integer integer2) { this.integer2 = integer2; } public void setInteger(Integer integer) { this.integer = integer; } @Relevancy({\"integer\",\"integer2\"}) public void test(int a,int b){ integer=a+b; } } 最后测试代码如下：\nTestModel model1=new TestModel(); model1.setInteger(13); TestModel model= ValCheckProxyFactory.getProxyInstance(model1, ValCheckInterceptor.I); model.setInteger(566); 校验成功：\nlearning.proxy.ValidateException: [learning.proxy.TestModel$$EnhancerByCGLIB$$bafbcd1.integer] validation [ValMax] failed: value should \u003c= 100.0, value=566 ","permalink":"/posts/tencent-2351653/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-05-05；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n不同于jdk自带的动态代理只能针对接口生成代理类，cglib可以对类实现代理，实现也非常的傻瓜式，我们只需要实现net.sf.cglib.proxy.MethodInterceptor接口，使用方法与jdk自带的动态代理类似。\n","title":"Java字节码深挖 第二站：CGLIB"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-05-05；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\njavassist是真正的可以对class为所欲为，甚至可以生成真正的class文件，它将字节码操作过程全部封装了起来，我们可以直接使用符合java规范的String直接修改，做到了动态修改代码跟修改字符串一样方便。并且，javassist的接口也简洁明了，操作难度跟反射没什么区别，我就不赘述了，具体接口看下面样例代码。（目前还没用javassist做出过啥好玩的东西，主要javassist是动态修改class，用起来隐隐约约感觉有点不安，而且目前项目生产方面好像还没有这方面的需求。。）\n预告：下一节我会说说JSR-269，编译时修改语法树，按照我们的规则生成想要的字节码。\n//获取类池 ClassPool cp = ClassPool.getDefault(); //获取类 CtClass cc = cp.get(\"learning.ATest\"); //获取对象 CtMethod m = cc.getDeclaredMethod(\"print\"); //修改方法体 m.setBody(\"{ System.out.println(\\\"running\\\"); }\"); //方法前插入 m.insertBefore(\"{ System.out.println(\\\"start\\\"); }\"); //方法后 m.insertAfter(\"{ System.out.println(\\\"end\\\"); }\"); //获取所有成员变量 CtField[] f=cc.getDeclaredFields(); for (CtField ctField : f) { //设置get方法 String methodStrF =\"public %s get%s(){\" +\" return %s;\" +\"}\"; String methodStr=String.format(methodStrF,ctField.getType().getSimpleName(),captureName(ctField.getName()),ctField.getName()); CtMethod newMethod = CtNewMethod.make(methodStr, cc); cc.addMethod(newMethod); //设置set方法 methodStrF =\"public void set%s(%s %s){\" +\" this.%s=%s;\" +\"}\"; methodStr=String.format(methodStrF,captureName(ctField.getName()),ctField.getType().getSimpleName(),ctField.getName(),ctField.getName(),ctField.getName()); newMethod = CtNewMethod.make(methodStr, cc); cc.addMethod(newMethod); } Class c = cc.toClass(); //生成class文件 cc.writeFile(); ATest h = (ATest)c.newInstance(); h.print(); 原ATest.java文件：\npublic class ATest implements Serializable { private static final long serialVersionUID = 140932343675039705L; private double aaa; private double ddd; private Double ccc; public void print(){ System.out.println(\"run...\"); } public void testRe(){ this.ddd=10; this.aaa=60; this.ccc=null; } private Object readResolve() { System.out.println(\"readResolve exec...\"); return this; } public void t(double a){} public void t(Double a){} } 生成的ATest.class文件：\n// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package learning; import java.io.Serializable; public class ATest implements Serializable { private static final long serialVersionUID = 140932343675039705L; private double aaa; private double ddd; private Double ccc; public ATest() { } public void print() { System.out.println(\"start\"); System.out.println(\"running\"); Object var2 = null; System.out.println(\"end\"); } public void testRe() { this.ddd = 10.0D; this.aaa = 60.0D; this.ccc = null; } private Object readResolve() { System.out.println(\"readResolve exec...\"); return this; } public void t(double a) { } public void t(Double a) { } public long getSerialVersionUID() { return 140932343675039705L; } public void setSerialVersionUID(long var1) { serialVersionUID = var1; } public double getAaa() { return this.aaa; } public void setAaa(double var1) { this.aaa = var1; } public double getDdd() { return this.ddd; } public void setDdd(double var1) { this.ddd = var1; } public Double getCcc() { return this.ccc; } public void setCcc(Double var1) { this.ccc = var1; } } ","permalink":"/posts/tencent-2351654/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-05-05；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\njavassist是真正的可以对class为所欲为，甚至可以生成真正的class文件，它将字节码操作过程全部封装了起来，我们可以直接使用符合java规范的String直接修改，做到了动态修改代码跟修改字符串一样方便。并且，javassist的接口也简洁明了，操作难度跟反射没什么区别，我就不赘述了，具体接口看下面样例代码。（目前还没用javassist做出过啥好玩的东西，主要javassist是动态修改class，用起来隐隐约约感觉有点不安，而且目前项目生产方面好像还没有这方面的需求。。）\n","title":"Java字节码深挖 第三站：Javassist"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-03-22；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n严格来说，代理模式并不能算我们真的设计到了字节码的概念，字节码相关的操作都被Proxy类封装了起来，提供了Proxy.newProxyInstance(ClassLoader loader, Class\u003c?\u003e[] interfaces, InvocationHandler h)接口供我们调用，我们只需要考虑业务相关逻辑即可，实际开发中使用动态代理的时候，反射的知识其实更重要。\n实际的使用也非常简单粗暴，从Proxy.newProxyInstance需要传入三个参数便可大概猜出它的作用：第一个参数是类加载器ClassLoader，我们需要为代理类指定一个类加载器，实际开发中一般直接指定AppClassLoader就行；第二个参数是接口列表，将你需要代理的接口列表传入即可，注意是数组，我们可以传入多个接口；第三个参数是调用处理器，传入了我们的逻辑实现，先我将细说这个InvocationHandle。\n我们需要实现一个InvocationHandler 接口，它里面只包含了一个方法：public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;我们调用newProxyInstance 得到的对象在执行方法的时候，本身并不做逻辑操作，真正的逻辑实现在了这个方法内部，代理对象会将自己本身作为第一个参数，被调用的方法作为第二个参数，你调用代理对象方法时输入的参数作为第三个参数来调用你传入的InvocationHandler.invoke方法。在这个方法里，你可以用反射来得到你想得到的信息，并实现真正的业务逻辑。\n下面我将举一个简单的实战例子：\n@HttpClient(\"https://blog.linhui.fun/\") public interface TestHttpClient { @HttpRequest(value = \"/attachment/20200609/cc4cfce1f93949efb6c3af5adb16b2a9.png\", method = \"GET\") Object getPicTure(@RequestBody Map request,@Param(\"key\") String key); @HttpRequest(value = \"/test/post\", method = \"POST\") Object postRequest(@RequestBody ReqClass request); static OtcTradeHttpClient getInstance(){ return HttpClientFactory.getIns(OtcTradeHttpClient.class); } } HttpClientFactory中获取代理实例的代码如下：\npublic static \u003cT\u003e T getIns(Class\u003cT\u003e clz) { if (insCache.containsKey(clz)) return (T) insCache.get(clz).t; if (!clz.isInterface()) throw new RuntimeException(\"入参应为接口类\"); HttpClient aHC = Reflections.getAnnotation(clz, HttpClient.class); if (aHC == null) throw new RuntimeException(\"HttpClient接口应带有HttpClient注解\"); HttpClientHandler\u003cT\u003e httpClientHandler = new HttpClientHandler\u003c\u003e(); switch (aHC.type()) { case URL: httpClientHandler.baseUrl = aHC.value(); break; case PROPERTY_PARAM: httpClientHandler.baseUrl = PropertyUtil.getString(aHC.value()); break; } Map\u003cMethod, Function\u003cObject[], Object\u003e\u003e methodFunctionMap = initMethodFunctionMap(httpClientHandler.baseUrl, clz); httpClientHandler.methodFunctionMap = methodFunctionMap; InvocationHandler handler = (proxy, method1, args) -\u003e methodFunctionMap.get(method1).apply(args); httpClientHandler.t = (T) Proxy.newProxyInstance(clz.getClassLoader(), new Class\u003c?\u003e[]{clz}, handler); insCache.putIfAbsent(clz, httpClientHandler); return (T) insCache.get(clz).t; } 其中initMethodFunctionMap方法是根据接口类的class对象通过反射获取相关信息得到一个Method-\u003eFunction的Map，Function中对该Method的方法中带@RequestBody注解的参数进行转为Map加入请求Json中，将@Param注解参数作为参数直接加入请求Json中，最后将生成的Json字符串通过@HttpRequest指定的方法和url相对路径发起请求，对得到的字符串进行反序列化成回参并返回，从而封装了Http客户端请求框架。\n这种方法只用了不到100行代码，实现了整个Http客户端的封装。开发者在进行Http客户端请求开发的时候再也不需要自己来封装请求参数和考虑序列化问题。\n以上就是动态代理的一种用法，提供思路，仅供参考。\n","permalink":"/posts/tencent-2351652/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-03-22；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n严格来说，代理模式并不能算我们真的设计到了字节码的概念，字节码相关的操作都被Proxy类封装了起来，提供了Proxy.newProxyInstance(ClassLoader loader, Class\u003c?\u003e[] interfaces, InvocationHandler h)接口供我们调用，我们只需要考虑业务相关逻辑即可，实际开发中使用动态代理的时候，反射的知识其实更重要。\n","title":"Java字节码深挖 第一站：动态代理"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-03-04；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n代理服务第一版速度挺猛，但是满载的时候内存消耗到无法忍受，最高的时候吃了我1.8G内存，后经过一系列调优操作，目前满载内存消耗约为100MB。\n首先，Netty本身的零拷贝技术理论上内存消耗不大，但是经过对jvm堆分析发现netty的ByteBuf占用了大量的空间，查阅文档后发现Netty默认会开启缓存策略，将对象复用来提高性能，我的需求是性能差不多得了，内存消耗低才是王道，所以关了。\n其次，我分析jvm堆的时候还发现占用最高的是byte[]对象，而大量使用byte对象的场景只有编解码以及加解密的时候，虽然byte[]用完就丢gc也能处理，但是众所周知gc并不会及时清理垃圾，而且频繁gc代价可不小。\n所以我使用了对象池技术，byte[]对象只能在对象池中获取，对象池大小可以在极小和极大值之间动态扩张。\nprotected void encode(ChannelHandlerContext ctx, TcpMessage msg, ByteBuf out) throws Exception { if (msg.getBodyBuf() != null) { byte[] data=msg.getBodyBuf(); byte[] ciphertext=ByteArrayPool.getIns().getByteArray(data.length); EncryptUtil.AESEncode(data, data.length, ciphertext, configProperties.getDesKey()); msg.setEncryptBodyBuf(ciphertext); ByteArrayPool.getIns().returnByteArray(data); } byte[] headBuf = msg.getfHead().Serialize(); byte[] ciphertext=ByteArrayPool.getIns().getByteArray(headBuf.length); EncryptUtil.AESEncode(headBuf, headBuf.length, ciphertext, configProperties.getDesKey()); EncryptUtil.selfWriteByte(out,ciphertext,ciphertext.length,configProperties); if (msg.getBodyBuf() != null) { EncryptUtil.selfWriteByte(out,msg.getBodyBuf(),msg.getBodyBuf().length,configProperties); } ByteArrayPool.getIns().returnByteArray(headBuf); ByteArrayPool.getIns().returnByteArray(msg.getBodyBuf()); } protected void decode(ChannelHandlerContext ctx, ByteBuf in, List\u003cObject\u003e out) throws Exception { TcpMessage tcpMessage = new TcpMessage(); try { in.markReaderIndex(); if (in.readableBytes() \u003c TcpHead.headLength) { in.resetReaderIndex(); return; } TcpHead tcpHead = new TcpHead(); byte[] ciphertext = ByteArrayPool.getIns().getByteArray(TcpHead.headLength); byte[] headByte = ByteArrayPool.getIns().getByteArray(TcpHead.headLength, true); if (!EncryptUtil.selfReadByte(in,ciphertext,ciphertext.length,configProperties)){ //读取失败，长度不够 in.resetReaderIndex(); ByteArrayPool.getIns().returnByteArray(ciphertext); ByteArrayPool.getIns().returnByteArray(headByte); return; } EncryptUtil.AESDecode(ciphertext, ciphertext.length, headByte, configProperties.getDesKey()); tcpHead.DeSerialize(ByteBuffer.wrap(headByte)); ByteArrayPool.getIns().returnByteArray(ciphertext); ByteArrayPool.getIns().returnByteArray(headByte); tcpMessage.setfHead(tcpHead); if (tcpHead.getCrc() != tcpHead.calCrc()) { ctx.channel().close(); System.out.println(\"CRC校验失败\"); } if (in.readableBytes() \u003c tcpHead.getLength()) { in.resetReaderIndex(); return; } if (tcpHead.getRealLength() \u003e 0) { byte[] mainbuf = ByteArrayPool.getIns().getByteArray(tcpHead.getRealLength()); ciphertext = ByteArrayPool.getIns().getByteArray(tcpHead.getLength(), true); if (!EncryptUtil.selfReadByte(in,ciphertext,ciphertext.length,configProperties)){ //读取失败，长度不够 in.resetReaderIndex(); ByteArrayPool.getIns().returnByteArray(ciphertext); ByteArrayPool.getIns().returnByteArray(mainbuf); return; } EncryptUtil.AESDecode(ciphertext, ciphertext.length, mainbuf, configProperties.getDesKey()); ByteArrayPool.getIns().returnByteArray(ciphertext); tcpMessage.setBodyBuf(mainbuf, tcpHead.getRealLength()); } out.add(tcpMessage); } catch (Exception ex) { // Logger.LogError(ex.getMessage(), ex); } } public class ByteArrayPool { int minN = 100; int maxN = 500; int mod = 16; private static class Ins { private static ByteArrayPool I = new ByteArrayPool(); } public static ByteArrayPool getIns() { return Ins.I; } private Map\u003cInteger, LinkedBlockingQueue\u003cbyte[]\u003e\u003e pool = new ConcurrentHashMap\u003c\u003e(); public byte[] getByteArray(int length, boolean isNotUpper) { if (isNotUpper) return get(length); length += mod - length % mod; return get(length); } public byte[] getByteArray(int length) { return getByteArray(length, false); } private byte[] get(int length) { byte[] ret; poolDataMap.computeIfAbsent(length,k-\u003enew AtomicInteger(0)); pool.computeIfAbsent(length, k -\u003e new LinkedBlockingQueue\u003c\u003e()); LinkedBlockingQueue\u003cbyte[]\u003e queue = pool.get(length); try { if (poolDataMap.get(length).get()\u003c=minN){ poolDataMap.get(length).addAndGet(1); return new byte[length]; } ret= queue.poll(); if (ret==null){ if (length\u003c50){ poolDataMap.get(length).addAndGet(1); return new byte[length]; } Thread.sleep(10); poolDataMap.get(length).addAndGet(1); return new byte[length]; } return ret; } catch (Throwable exception) { poolDataMap.get(length).addAndGet(1); return new byte[length]; } } public static Map\u003cInteger, AtomicInteger\u003e poolDataMap=new ConcurrentHashMap\u003c\u003e(); public void returnByteArray(byte[] bytes) { if (bytes == null) return; pool.computeIfAbsent(bytes.length, k -\u003e new LinkedBlockingQueue\u003c\u003e()); LinkedBlockingQueue\u003cbyte[]\u003e queue = pool.get(bytes.length); if (queue.size() \u003e maxN) { poolDataMap.computeIfAbsent(bytes.length,k-\u003enew AtomicInteger(0)); poolDataMap.get(bytes.length).addAndGet(-1); System.gc(); return; } queue.offer(bytes); } } 另外cipher.doFinal是可以传入byte[]来接收加解密结果的，这样我们加解密时候cipher就不会浪费我们多余空间了。\ncipher.doFinal(data, 0, length, result, 0); 最后就是jvm启动的时候，我换了cms来做gc，因为cms提供了一个fullGc的参数，老年代内存占用达到临界值就强行fullGc，而且cms管理小内存貌似不错，我把临界值设置为70%。\n做完这些后把jvm堆大小上限设置为100M，测试后发现我的500M带宽能跑满，内存占用不到100M，收工！\n","permalink":"/posts/tencent-2351629/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-03-04；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n代理服务第一版速度挺猛，但是满载的时候内存消耗到无法忍受，最高的时候吃了我1.8G内存，后经过一系列调优操作，目前满载内存消耗约为100MB。\n","title":"代理服务内存调优记录"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-03-04；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n原解在：http://www.linhui.fun/article/47\n题面是一个装逼小气鬼送礼物，他有一个架子，有n排礼物（每个礼物有自己的权值c，c越大礼物越贵），他要送m个礼物出去，但是他又不想送最贵的m个又想装大方，所以他给自己立了个规矩，每次选礼物都只能从任意一排的两端选择，问他最贵能送多少权值出去？ 输入格式：第一行输入n，m ，下面n行每行输入一个x代表这一行有x个礼物，后面跟x个数字，每个数字代表这个位置的礼物的权值。 输出：一个数字，代表最贵的权值。 发现计算b数组的时候也可以动态规划算，并且可以优化为一维。\n另外算dp数组的时候不需要复制，也不需要遍历到m（到本行最大数量即可）\n#include \u003ciostream\u003e #include\u003cstdio.h\u003e int c[105]; int b[105]; int dp[10005]; int main() { int n, m, x; scanf(\"%d%d\", \u0026 n, \u0026m); for (int i = 0; i \u003c n; i++) { scanf(\"%d\", \u0026 c[0]); for (int j = 1; j \u003c= c[0]; j++) { scanf(\"%d\", \u0026 x); c[j] = j \u003e 1 ? c[j - 1] + x : x; for (int k = j; k \u003e= 1; k--) { b[k] = b[k - 1] + x \u003e c[k] ? b[k - 1] + x : c[k]; } } for (int j = m; j \u003e= 1; j--) { for (int k = 1; k \u003c= c[0] \u0026\u0026 k \u003c= j; k++) { int temp = dp[j - k] + b[k]; dp[j] = temp \u003e dp[j] ? temp : dp[j]; } } } printf(\"%d\\n\", dp[m]); } ","permalink":"/posts/tencent-2351655/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-03-04；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n原解在：http://www.linhui.fun/article/47\n题面是一个装逼小气鬼送礼物，他有一个架子，有n排礼物（每个礼物有自己的权值c，c越大礼物越贵），他要送m个礼物出去，但是他又不想送最贵的m个又想装大方，所以他给自己立了个规矩，每次选礼物都只能从任意一排的两端选择，问他最贵能送多少权值出去？ 输入格式：第一行输入n，m ，下面n行每行输入一个x代表这一行有x个礼物，后面跟x个数字，每个数字代表这个位置的礼物的权值。 输出：一个数字，代表最贵的权值。 发现计算b数组的时候也可以动态规划算，并且可以优化为一维。\n","title":"小气鬼送礼物算法优化-（2020阿里笔试题）"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-03-03；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n首先声明：本文内容仅作为技术研究，学习者不得用于违法用途，本文涉及到的源码仅展示部分核心代码以供学习，全部的功能代码不会开源！！！ 分析\n网络代理的原理非常简单，源地址访问目标地址的时候流量并不直接传输，而是将目标地址信息和流量包发送到代理服务器上，由代理服务器进行转发，从而实现网络代理功能。\n通过分析可以得知，我们的代理服务器对外只需要满足3个基础功能即可：1.连接建立 2.字节流转发 3.连接断开。当然为了服务功能的完善，我们额外需要一个4.登录功能。\n同时，我们还可以分析得知，一般源服务器并不会只通过代理建立一个连接，所以我们还需要有个连接管理功能，所以协议内需要带上连接相关信息，我们可以用一个int来表示。\n报文设计\n初定报文：\n所以，我们的协议就拟定了：\n报文头：\n连接编号 4字节\n指令 4字节\n报文体长度 4字节\ncrc校验 4字节\n报文体：\n数据内容，长度不定\npublic enum FUNC_ENUM { CONN(0x01,\"连接目标地址\"), DIS_CONN(0x02,\"断开目标地址\"), SEND(0x03,\"转发数据\"), LOGIN(0x04,\"登录\"), ; private int code; private String desc; private static final class Mapping{ private final static Map\u003cInteger, FUNC_ENUM\u003e code2Enum=new HashMap\u003c\u003e(); } FUNC_ENUM(int code, String desc) { this.code = code; this.desc = desc; Mapping.code2Enum.put(this.code,this); } public static FUNC_ENUM getByCode(int code){ return Mapping.code2Enum.get(code); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } public class TcpHead { /** * 包头长度 */ public static int headLength = 20; private int sessionId; private int funcId; private int length; private int realLength=0; private int crc; public byte[] Serialize() { byte[] bHead = null; try { ByteBuffer byteBuffer = ByteBuffer.wrap(ByteArrayPool.getIns().getByteArray(headLength,true)).order(ByteOrder.BIG_ENDIAN); byteBuffer.putInt(sessionId); byteBuffer.putInt(funcId); byteBuffer.putInt(length); byteBuffer.putInt(realLength); byteBuffer.putInt(crc= calCrc()); bHead = byteBuffer.array(); } catch (Exception ex) { ex.printStackTrace(); } return bHead; } public void DeSerialize(ByteBuf byteBuf){ sessionId=byteBuf.readInt(); funcId=byteBuf.readInt(); length=byteBuf.readInt(); realLength =byteBuf.readInt(); crc=byteBuf.readInt(); } public void DeSerialize(ByteBuffer byteBuffer){ sessionId=byteBuffer.getInt(); funcId=byteBuffer.getInt(); length=byteBuffer.getInt(); realLength =byteBuffer.getInt(); crc=byteBuffer.getInt(); } public int calCrc(){ ByteBuffer byteBuffer = ByteBuffer.allocate(headLength-4).order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putInt(sessionId); byteBuffer.putInt(funcId); byteBuffer.putInt(length); byteBuffer.putInt(realLength); return CrcUtils.getCrc32ByData(byteBuffer.array()); } } public class TcpMessage { public TcpMessage() { } public TcpMessage(int funcId, int sessionId) { tcpHead = new TcpHead(); tcpHead.setFuncId(funcId); tcpHead.setSessionId(sessionId); } /** * 协议包头 */ private TcpHead tcpHead; /** * 协议包体 */ private byte[] bodyBuf; public TcpHead getfHead() { return tcpHead; } public void setfHead(TcpHead tcpHead) { this.tcpHead = tcpHead; } public byte[] getBodyBuf() { return bodyBuf; } public void setBodyBuf(byte[] bodyBuf,int length) { this.bodyBuf = bodyBuf; this.tcpHead.setRealLength(length); } public void setEncryptBodyBuf(byte[] bodyBuf) { this.tcpHead.setLength(bodyBuf.length); this.bodyBuf = bodyBuf; } } 加密报文\n报文直接进行传输并不安全，很有可能导致数据泄露问题，所以我们需要进行加密，此处我采用了AES-256加密算法，采用了时间（小时单位）+初始密码的形式作为AES密码对报文进行加密（包括报文头），所以我们的报文头需要增加一个字段：**报文体实际长度，**如下：\n报文头：\n连接编号 4字节\n指令 4字节\n报文体长度 4字节\n报文体实际长度 4字节\ncrc校验 4字节\n报文体：\n数据内容，长度不定\n加密后的报文头长度固定为32字节，报文体则会被填充至16的整倍数。\npublic static void AESEncode(byte[] data, int length, byte[] result, byte[] keyBytes) { try { if (aesKey == null) setAesKey(keyBytes); Cipher cipher = Cipher.getInstance(\"AES\"); cipher.init(Cipher.ENCRYPT_MODE, key); cipher.doFinal(data, 0, length, result, 0); } catch (Exception e) { e.printStackTrace(); } } public static void AESDecode(byte[] data, int length, byte[] result, byte[] keyBytes) { try { if (aesKey == null) setAesKey(keyBytes); Cipher cipher = Cipher.getInstance(\"AES\"); cipher.init(Cipher.DECRYPT_MODE, key); cipher.doFinal(data, 0, length, result, 0); } catch (Exception e) { e.printStackTrace(); } } 混淆报文：\n由于AES加密后的数据长度非常具有特征（16倍数），并且加密后的报文可能被特征识别，所以我们对报文再次进行混淆，混淆的算法简单粗暴，如下：\n根据密码生成固定的一组数字，根据这组数据在报文体和报文头插入随机数据，比例为20%\n如生成的数字为5 6,3,10,15,20….\n其中第0位数字比较特殊\n若第0位数为奇数，则先传输3个有效数据，再传输10个随机数据，再传输15个有效数字……随机数据占比20%\n若第0位数为偶数，则先传输6个随机数据，传输3个有效数据，再传输10个随机数据，再传输15个有效数字……随机数据占比20%\npublic static boolean selfReadByte(ByteBuf in, byte[] result, int readLength, ConfigProperties configProperties) { int index = 0; int i = configProperties.getRandomByKey(0,2)+ 1; Set\u003cByteBuf\u003e waitForClearSet = new HashSet\u003c\u003e(); while (index \u003c readLength) { if (i % 2 == 0) {//除去无用字节 int readByteL = configProperties.getRandomByKey(i, readLength / 10) + 5; if (in.readableBytes() \u003c readByteL) return false; waitForClearSet.add(in.readBytes(readByteL)); } else { int readByteL = configProperties.getRandomByKey(i, readLength / 2) + 5; if (index \u003e readLength - readByteL) readByteL = readLength - index; if (in.readableBytes() \u003c readByteL) return false; in.readBytes(result, index, readByteL); index += readByteL; } i++; } for (ByteBuf byteBuf : waitForClearSet) { byteBuf.release(); } return true; } public static boolean selfWriteByte(ByteBuf out, byte[] data, int writeLength, ConfigProperties configProperties) { int index = 0; int i = configProperties.getRandomByKey(0,2)+ 1; while (index \u003c writeLength) { if (i % 2 == 0) {//写入随机字节 int readByteL = configProperties.getRandomByKey(i, writeLength / 10) + 5; byte[] bytes = configProperties.getRandomBytes(readByteL); out.writeBytes(bytes, 0, readByteL); ByteArrayPool.getIns().returnByteArray(bytes); } else { int readByteL = configProperties.getRandomByKey(i, writeLength / 2) + 5; if (index \u003e writeLength - readByteL) readByteL = writeLength - index; out.writeBytes(data, index, readByteL); index += readByteL; } i++; } return true; } 结构设计\nNetty是一个非常流行的网络io框架，内置了Sock5的编解码类，我们设计的VPN客户端对外提供的接口便是Sock5接口，便于浏览器的接入。\n因此我们的目标便清晰了，我们的客户端作为sock5服务端在本地打开sock5服务端口便于其他软件接入，客户端收到sock5的连接请求后将其转换为私有协议（上面设计的报文）传输给服务端，服务端与目标服务器建立连接后通知客户端，之后服务端/客户端收到的数据流包装为报文后发送给对方，对方根据连接编号找到实际的连接写入即可。\n首先是登录功能：\n这里客户端只需要把账号密码发送给服务端即可。\npublic class PasswordAuthRequestInboundHandler extends SimpleChannelInboundHandler\u003cTcpMessage\u003e { private final ConfigUtil configUtil; @Override protected void channelRead0(ChannelHandlerContext ctx, TcpMessage msg) throws Exception { byte[] data = msg.getBodyBuf(); if (msg.getfHead().getFuncId() == FUNC_ENUM.LOGIN.getCode() \u0026\u0026 data.length \u003e= UsernameAndPasswordModel.length) { UsernameAndPasswordModel usernameAndPasswordModel = new UsernameAndPasswordModel(); usernameAndPasswordModel.userName = BytesUtils.getString(data, 0, UsernameAndPasswordModel.userNameLength, StandardCharsets.UTF_8); usernameAndPasswordModel.password = BytesUtils.getString(data, UsernameAndPasswordModel.userNameLength, UsernameAndPasswordModel.passwordLength, StandardCharsets.UTF_8); //认证成功 if (configUtil.getUsers().get(usernameAndPasswordModel.userName).equals(usernameAndPasswordModel.password)) { byte[] bytes=\"succeed\".getBytes(StandardCharsets.UTF_8); msg.setBodyBuf(bytes,bytes.length); ctx.writeAndFlush(msg); ctx.pipeline().remove(this); System.out.println(\"客户端登录成功\"); return; } } byte[] bytes=\"fail\".getBytes(StandardCharsets.UTF_8); msg.setBodyBuf(bytes,bytes.length); //发送鉴权失败消息，完成后关闭channel ctx.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE); } } 功能指令开发-发送连接服务器指令（客户端）：\npublic void connDest(ChannelHandlerContext clientCtx, String ip, int port, Socks5AddressType socks5AddressType) { int sessionId = sessionIdGenerate.addAndGet(1); ConnDestModel connDestModel = new ConnDestModel(sessionId); connDestModel.ip = ip; connDestModel.port = port; TcpMessage message = new TcpMessage(FUNC_ENUM.CONN.getCode(), sessionId); byte[] bytes = connDestModel.toBytes(); message.setBodyBuf(bytes, bytes.length); send(message); sessionId2CtxMap.put(sessionId, clientCtx.channel()); ctx2SessionIdMap.put(clientCtx.channel(), sessionId); sessionId2Sock5TypeMap.put(sessionId, socks5AddressType); } 功能指令开发-连接服务器（服务端）：\nprivate void connDest(ChannelHandlerContext ctx, ConnDestModel data) { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(clientWorkGroup); bootstrap.channel(NioSocketChannel.class); bootstrap.option(ChannelOption.TCP_NODELAY, true); ServerInboundHandler serverInboundHandler=this; bootstrap.handler(new ChannelInitializer\u003cSocketChannel\u003e() { @Override protected void initChannel(SocketChannel ch) throws Exception { //添加服务端写客户端的Handler ch.pipeline() .addLast(new ByteDecode()) .addLast(new ByteEncode()) .addLast(new ReceiveDestInboundHandler(ctx.channel(), data.sessionId,serverInboundHandler,ctx)); } }); ChannelFuture future = bootstrap.connect(data.ip, data.port); future.addListener((ChannelFutureListener) future1 -\u003e { if (future1.isSuccess()) { log.debug(\"目标服务器连接成功\"); ctx.channel().writeAndFlush(getConnMsg(data.sessionId)); sessionId2CtxMap.put(data.sessionId,future1.channel()); ctx2SessionIdMap.put(future1.channel(),data.sessionId); } else { log.error(\"连接目标服务器失败,address={},port={}\", data.ip, data.port); ctx.channel().writeAndFlush(getDisConnMsg(data.sessionId)); future1.channel().close(); } }); } 功能指令开发-流量转发（客户端）：\nprotected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteModel data) throws Exception { sendMsg(channelHandlerContext.channel(),data); } public void sendMsg(Channel clientCtx, ByteModel data) { int sessionId = ctx2SessionIdMap.get(clientCtx); TcpMessage message = new TcpMessage(FUNC_ENUM.SEND.getCode(), sessionId); message.setBodyBuf(data.getData(), data.getLength()); send(message); } private void send(TcpMessage message) { if (!isLogin.get()) return; if (!serverChannel.isActive() || !serverChannel.isOpen()) { connServer(); } serverChannel.writeAndFlush(message); } 功能指令开发-流量转发（服务端）：\nChannel clientCh; int sessionId; public ReceiveDestInboundHandler(Channel clientCh, int sessionId) { this.clientCh = clientCh; this.sessionId=sessionId; } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteModel data) throws Exception { TcpMessage message=new TcpMessage(FUNC_ENUM.SEND.getCode(),sessionId); message.setBodyBuf(data.getData(),data.getLength()); clientCh.writeAndFlush(message); } 指令功能开发-断开连接（客户端）：\npublic void disConnDest(int sessionId) { disConnDest(sessionId, sessionId2CtxMap.get(sessionId)); } public void disConnDest(Channel clientCtx) { if (clientCtx == null) return; int sessionId = ctx2SessionIdMap.getOrDefault(clientCtx, -1); disConnDest(sessionId, clientCtx); } private void disConnDest(int sessionId, Channel clientCtx) { if (clientCtx == null || sessionId == -1) return; TcpMessage message = new TcpMessage(FUNC_ENUM.DIS_CONN.getCode(), sessionId); send(message); clientCtx.close(); ctx2SessionIdMap.remove(clientCtx); sessionId2CtxMap.remove(sessionId); } 指令功能开发-断开连接（服务端）：\npublic void disConn(ChannelHandlerContext ctx, int sessionId) { Channel dest = sessionId2CtxMap.get(sessionId); if (dest != null) { dest.close(); sessionId2CtxMap.remove(sessionId); ctx2SessionIdMap.remove(dest); ctx.writeAndFlush(getDisConnMsg(sessionId)); } } private TcpMessage getDisConnMsg(int sessionId) { TcpHead tcpHead = new TcpHead(); tcpHead.setFuncId(FUNC_ENUM.DIS_CONN.getCode()); tcpHead.setSessionId(sessionId); TcpMessage message = new TcpMessage(); message.setfHead(tcpHead); return message; } 到此，我们的服务写完了，稍微测了一下性能有点问题，居然占用了100M内存，通过分析发现是流量包转发的时候new了太多的byte数组了，因此我们改造了一下编解码的地方，使用了byte数组池的方式复用了byte数组：\npublic class ByteArrayPool { int minN = 10; int maxN = 1000; int mod=16; private static class Ins { private static ByteArrayPool I = new ByteArrayPool(); } public static ByteArrayPool getIns() { return Ins.I; } private Map\u003cInteger, LinkedBlockingQueue\u003cbyte[]\u003e\u003e pool = new ConcurrentHashMap\u003c\u003e(); public byte[] getByteArray(int length, boolean isNotUpper) { if (isNotUpper) return get(length); length += mod - length % mod; return get(length); } public byte[] getByteArray(int length) { return getByteArray(length, false); } private byte[] get(int length) { pool.computeIfAbsent(length, k -\u003e new LinkedBlockingQueue\u003c\u003e()); LinkedBlockingQueue\u003cbyte[]\u003e queue = pool.get(length); if (queue.size() \u003c minN) return new byte[length]; else { try { return queue.take(); } catch (InterruptedException exception) { return new byte[length]; } } } public void returnByteArray(byte[] bytes){ if (bytes==null)return; pool.computeIfAbsent(bytes.length, k -\u003e new LinkedBlockingQueue\u003c\u003e()); LinkedBlockingQueue\u003cbyte[]\u003e queue = pool.get(bytes.length); if (queue.size()\u003emaxN)return; queue.offer(bytes); } } 编解码改造：\nprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List\u003cObject\u003e out) throws Exception { TcpMessage tcpMessage = new TcpMessage(); try { in.markReaderIndex(); if (in.readableBytes() \u003c TcpHead.headLength) { in.resetReaderIndex(); return; } TcpHead tcpHead = new TcpHead(); byte[] ciphertext = ByteArrayPool.getIns().getByteArray(TcpHead.headLength); byte[] headByte = ByteArrayPool.getIns().getByteArray(TcpHead.headLength, true); if (!EncryptUtil.selfReadByte(in,ciphertext,ciphertext.length,configProperties)){ //读取失败，长度不够 in.resetReaderIndex(); return; } EncryptUtil.AESDecode(ciphertext, ciphertext.length, headByte, configProperties.getDesKey()); tcpHead.DeSerialize(ByteBuffer.wrap(headByte)); ByteArrayPool.getIns().returnByteArray(ciphertext); ByteArrayPool.getIns().returnByteArray(headByte); tcpMessage.setfHead(tcpHead); if (tcpHead.getCrc() != tcpHead.calCrc()) { System.out.println(\"CRC校验失败\"); } if (in.readableBytes() \u003c tcpHead.getLength()) { in.resetReaderIndex(); return; } if (tcpHead.getRealLength() \u003e 0) { byte[] mainbuf = ByteArrayPool.getIns().getByteArray(tcpHead.getRealLength()); ciphertext = ByteArrayPool.getIns().getByteArray(tcpHead.getLength(), true); if (!EncryptUtil.selfReadByte(in,ciphertext,ciphertext.length,configProperties)){ //读取失败，长度不够 in.resetReaderIndex(); return; } name)); out.add(tcpMessage); } catch (Exception ex) { // Logger.LogError(ex.getMessage(), ex); } } protected void encode(ChannelHandlerContext ctx, TcpMessage msg, ByteBuf out) throws Exception { if (msg.getBodyBuf() != null) { byte[] data=msg.getBodyBuf(); byte[] ciphertext=ByteArrayPool.getIns().getByteArray(data.length); EncryptUtil.AESEncode(data, data.length, ciphertext, configProperties.getDesKey()); msg.setEncryptBodyBuf(ciphertext); ByteArrayPool.getIns().returnByteArray(data); } byte[] headBuf = msg.getfHead().Serialize(); byte[] ciphertext=ByteArrayPool.getIns().getByteArray(headBuf.length); EncryptUtil.AESEncode(headBuf, headBuf.length, ciphertext, configProperties.getDesKey()); EncryptUtil.selfWriteByte(out,ciphertext,ciphertext.length,configProperties); if (msg.getBodyBuf() != null) { EncryptUtil.selfWriteByte(out,msg.getBodyBuf(),msg.getBodyBuf().length,configProperties); } ByteArrayPool.getIns().returnByteArray(headBuf); ByteArrayPool.getIns().returnByteArray(msg.getBodyBuf()); } 大功告成！\n","permalink":"/posts/tencent-2351630/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-03-03；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n首先声明：本文内容仅作为技术研究，学习者不得用于违法用途，本文涉及到的源码仅展示部分核心代码以供学习，全部的功能代码不会开源！！！ 分析\n网络代理的原理非常简单，源地址访问目标地址的时候流量并不直接传输，而是将目标地址信息和流量包发送到代理服务器上，由代理服务器进行转发，从而实现网络代理功能。\n","title":"Netty手撸代理服务"},{"content":" 旧文迁移 · 补录说明\n原始发表：2022-02-21；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n简介 Raft是一种集群选举策略算法，用于保证集群的一致性。\nRaft将单节点的状态变化转为日志，通过日志同步和日志回放保证一致性。当少数节点挂掉集群依然可以对外提供服务。\nRaft是一个CP系统，牺牲了部分可用性（当leader切换时，服务短时间内不可用）。 节点启动\nRaft集群中每个节点刚刚启动时候会通过本地的日志回放完成状态机的初始化，后与其他节点互联，加入集群，此时节点身份为跟随者。Leader会对刚加入集群的节点发起日志同步操作，将自己的日志同步到这个节点（leader日志一定最新）。\n节点间互相保持心跳，心跳主要包含4个参数：当前任期-term、当前节点最大日志序号-maxIndex、当前节点已提交最大日志序号-maxCommitedIndex、当前领导者-leader。\n节点身份状态\n节点的身份分为三种，跟随者、候选人、领导者。\n一个正常的系统应该有一个领导人，其余节点为跟随者。\n当某节点触发了选举条件时，节点身份转化为候选人，候选人赢得选举成为领导者，否则退回跟随者。\n当一个节点收到了一个比自己任期大的心跳时，无论当前处于任何状态，都应该立即采用对方任期，并且成为跟随者。\n来自Leader的心跳会刷新节点内的一个用以触发选举的超时定时器（下文称为候选定时器）。\nLeader选举的触发\n节点进入集群时都是跟随者身份，由leader持续发送心跳刷新节点内的候选定时器，当节点一段时间没收到leader的心跳导致候选定时器超时，则节点身份转化为候选人。\n定时器超时并非一个定值，raft采用了随机延时巧妙的解决了split vote问题（即多个候选人同时开始选举，导致选票被多人瓜分无法快速选出领导者）。\nLeader选举过程\n候选人将任期加一后投票给自己，而后向所有节点发送拉票信息请求选票，并开始选举计时，超时未完成则视为选举失败。\n节点收到拉票信息后进行判断是否投票，若候选人任期不大于自己则拒绝投票（若和当前一样，则手上必然已经无票），若候选人日志序号比当前节点小则拒绝投票。否则投票给该节点，刷新候选定时器并更新任期。\n候选人在候选期间可能收到三种信息：\n跟随者的投票信息：代表该候选人票数加一，若此时票数大于集群节点数的一半，则选举成功，候选人成为领导者。\n其他候选人的拉票信息：若对方任期小于自己则无视，若对方日志序号不大于自己则无视，否则该候选人退出选举，选举失败。 自称领导者的心跳：若对方任期小于自己则无视，否则该选举人退出选举，选举失败。（若领导者任期不小于自己，由于过半复制原则，已提交日志序号不可能小于自己，但是未提交日志序号可能小于自己，此处可能导致丢失未提交日志，需要补偿措施）。 日志同步过程\n只有领导者可以写日志，并向其它节点发送新增的日志信息，Raft的日志同步遵循过半复制原则。\n领导者将状态机更新，并写日志，向其余节点发送新增日志信息。\n其余节点收到新增日志信息并应用到状态机后，向领导者响应（此处响应代表此条日志之前的日志也全部同步完毕，收到新增日志信息后依靠日志序号来判断是否连续，若不连续则从领导者同步之前的日志，最后再新增此条日志，保证有序）\n领导者收到一半以上的节点的响应后，将此条日志设为已提交日志，即已提交日志序号加一，并向其它节点推送该信息。\n其余节点收到最新的已提交序号后更新自己节点的已提交序号。\n通过过半复制原则可以保证被领导者commit的日志必然会被下一个领导者继承，因为在选举人投票的时候，一半以上的节点已经拥有该日志，不会投票给无此日志的节点。\n当新领导者当选时，若存在上一任遗留下来的未提交日志，无论是否有过半复制，一律认定为未提交日志，但不立即进行处理，而是当新的日志产生时，一起提交（序号X的日志被提交意味着序号小于X的日志已经全部提交）。\nRaft存在的问题\n未提交日志可能丢失，需要补偿机制\n当一个跟随者因为自己的问题（网络延迟），发起选举，该节点会成功顶替现有领导者，即使现有领导者功能正常。\n同一任期不会出现一个以上的领导者，但同一时间可能会出现两个领导者，Raft只能保证只有一个领导者可以提交日志（两个任期，新旧领导者，如网络故障导致集群分为两个分区，旧领导者在小分区，大分区有节点发起选举并成为新领导者）\nSOFAJRAFT的改进\n新增了预投票机制，候选人需要先发起一次预投票，解决了2问题，流程如下：\n节点候选计时器超时，节点成为候选人。\n该候选人向所有节点发起一次预投票。\n其他节点向当前领导者发起一次状态检测的请求，若当前领导者正常，则直接拒绝投票，否则按照原投票规则投票。\n候选人预投票获取一半以上的票数后，发起正式的投票，在预投票超时后依然没有获得一半以上的票数则退回跟随者状态，刷新候选计时器重新计时。\n领导者下台机制，当领导者在下台定时器超时前无法获得一半以上的节点的心跳响应时，领导者退位成为跟随者。\n我的理解：\n预投票机制保证了领导者无问题的时候阻止个别收不到领导者心跳的节点发起竞选，当其他节点收到预投票请求时候会检查当前领导者状态，此处若请求超时或者当前领导者状态异常，则进行投票。此机制结合领导者下台机制，我们会发现SOFAJRAFT用了一种很巧妙的方法解决了同一时间出现两个领导者的问题（问题3）：当候选人预选成功的时候，说明一半以上的节点请求当前领导者异常，请求超时的时间与领导者下台定时器的超时时间一致，当一半以上节点在预投票中投票给候选人时，意味着此时领导者已经触发下台机制，新领导者出现的时候旧领导者必然已经下台。\n","permalink":"/posts/tencent-2351632/","summary":" 旧文迁移 · 补录说明\n原始发表：2022-02-21；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n简介 Raft是一种集群选举策略算法，用于保证集群的一致性。\nRaft将单节点的状态变化转为日志，通过日志同步和日志回放保证一致性。当少数节点挂掉集群依然可以对外提供服务。\n","title":"分布式一致性算法-RAFT算法的理解和SOFA-RAFT的改进"},{"content":" 旧作补记\n原始时间：2022 年上海封控前期，具体日期不详\n本站补录：2026-10-12。\n壬寅岁初，大疫衅，九州皆困。\n居宅深，三千樱花，孤独自春。\n白衣持剑逆风行，风月同天共度瘟。\n天不仁，祸又栖藤县，飞鸟沉。\n俄乌争，战火焚。东南事，何时问！\n思绪纷，心怀惆怅转深？\n安坐愁忧卵无用，还需亲手换乾坤！\n待晴霁，神州俱欢颜，碧天云。\n","permalink":"/posts/hushangyi/","summary":" 旧作补记\n原始时间：2022 年上海封控前期，具体日期不详\n本站补录：2026-10-12。\n壬寅岁初，大疫衅，九州皆困。\n居宅深，三千樱花，孤独自春。\n白衣持剑逆风行，风月同天共度瘟。\n天不仁，祸又栖藤县，飞鸟沉。\n","title":"满江红·沪上疫"},{"content":" 旧文迁移 · 补录说明\n原始发表：2021-05-09；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n之前有 字符画视频 动态字符画java源代码 已经做了差不多了，今天运行了一下发现内存泄露，代码规范，线程安全……全都有问题，，改了改现在内存泄露好一些了（之前大点的视频一跑直接内存爆炸，太信任GC了），线程安全加了Lock目前来看没啥问题，，代码规范..能用就行，以后再改..。\n加了个新功能，直接导出MJPG格式的视频，然后可以用ffmpeg转换成mp4。\n具体使用方法如下：\n进入Draw，设置好视频地址，运行生成vedio文件（无音源） 进入Play可以播放vedio文件（无音源），可设置音源播放\n进入VedioToMjpg可以将vedio文件转为Mjpg视频文件（无音源）\n可以使用ffmpeg将Mjpg视频文件转为mp4 如：ffmpeg -i input.mjpg -i input.mp3 -c:v libx264 -strict -2 output.mp4\n","permalink":"/posts/tencent-2351637/","summary":" 旧文迁移 · 补录说明\n原始发表：2021-05-09；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n之前有 字符画视频 动态字符画java源代码 已经做了差不多了，今天运行了一下发现内存泄露，代码规范，线程安全……全都有问题，，改了改现在内存泄露好一些了（之前大点的视频一跑直接内存爆炸，太信任GC了），线程安全加了Lock目前来看没啥问题，，代码规范..能用就行，以后再改..。\n","title":"字符画视频 动态字符画java源代码2.0"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-09-11；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n昨天晚上（今天凌晨）睡前刷到鸿蒙2.0开源的消息，貌似放出了开发文档和IDE什么的（总之就是我们可以在上面写代码了），虽然我并不会安卓开发，但是不妨碍我写个HelloWorld呀~~，于是今天就来试试了~\n安装IDE，SDK，以及一些乱七八糟的依赖步骤在这：DevEco Studio下载安装\n编写HelloWorld的步骤在这：HarmonyOS的第一个程序\n看上去好像和（印象中的）安卓的开发没啥区别，除了xml里把Android换成ohos~~emmmmmmm，等大佬分析吧，如果是自己开发的话，这样可以兼容安卓的生态好像也不是啥坏事，如果是安卓套壳的话emmmmmmm（但愿不是）~\n另外，，不知道为啥下载Gradel什么的默认源还是在国外，还得挂SS下，…..“http://schemas.huawei.com/res/ohos”这个一开始一直标红，直接访问schemas.huawei.com却发现dns没有这个域名的ip，，然后挂SS下好Gradel后，又正常了...\n一大堆步骤后，总而言之，我跑起来咯~~\n","permalink":"/posts/tencent-2351639/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-09-11；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n昨天晚上（今天凌晨）睡前刷到鸿蒙2.0开源的消息，貌似放出了开发文档和IDE什么的（总之就是我们可以在上面写代码了），虽然我并不会安卓开发，但是不妨碍我写个HelloWorld呀~~，于是今天就来试试了~\n","title":"鸿蒙开发环境以及依赖的安装及HelloWorld编写"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-08-27；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n第一题是给你2,3,5三个数字，问你用这三个数字组成的数字按照大小排序，第n个是多少。（2,3,5,22,23,25,32,33,35,52,53,55,222,223……..）\n思路：1位数是3个，2位数是9个，k位数是3^k个，用n一路减下去，看看第n个是几位数，如第10个的话，10\u003e3,所以大于一位数，10-3=7,7\u003c=9,所以是两位数，两位数里的第7个，这题就转换成3进制中第7个数字是什么了（两位数），由于进制是0-base，所以7-1=6，一个个位算下去，6%3=0，所以第一位选择第0个的2，然后6/3=2，算第2位，2%3=2，所以选择第2个数5，最后结果是52~\n代码不贴了~\n第二题是很简单的dp问题，问你一个类型下面的图，一个球从上往下滚，每滚到一个数字就加上它，滚到最底层的时候，最大的数是多少？（只能向下或向左下右下滚）\n3\n2 5 7\n9 6 2 7 4\n6 7 4 7 9 5 10\n首先把这个图装换一下变成\n3\n2 5 7\n9 6 2 7 4\n6 7 4 7 9 5 10\n然后每个球只能往下，右下，右右下滚，就好做了~\ndp一波就行了~\n代码如下：\n#include \u003ciostream\u003e #include\u003cstdio.h\u003e #include\u003cstring\u003e #include \u003cvector\u003e #include \u003calgorithm\u003e #include \u003cstack\u003e #include \u003cset\u003e #include \u003cmap\u003e #include\u003cstring.h\u003e using namespace std; int dp[1000]; int a[1000]; int main() { int n; scanf(\"%d\", \u0026n); for (int i = 1; i \u003c= n; i++) { for (int j = 0; j \u003c 2 * i - 1; j++)scanf(\"%d\", \u0026a[j]); for (int j = 2 * i - 2; j \u003e=0; j--) { if (j \u003e= 2 \u0026\u0026 dp[j - 2] \u003e dp[j])dp[j] = dp[j - 2]; if (j \u003e= 1 \u0026\u0026 dp[j - 1] \u003e dp[j])dp[j] = dp[j - 1]; dp[j] += a[j]; } } int ans = dp[0]; for (int j = 0; j \u003c 2 * n - 1; j++)ans = ans \u003e dp[j] ? ans : dp[j]; printf(\"%d\\n\", ans); } ","permalink":"/posts/tencent-2351641/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-08-27；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n第一题是给你2,3,5三个数字，问你用这三个数字组成的数字按照大小排序，第n个是多少。（2,3,5,22,23,25,32,33,35,52,53,55,222,223……..）\n","title":"记京东笔试编程题目（JAVA开发）"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-08-12；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n今天刷了一下LeetCode，发现了一道很有感觉的题目，题面如下：\n看到这题的第一反应就是，编译原理里学到的自动机！！！\n然后我按照记忆里残留的编译原理的知识，开始画图，，，然后wa了。。。\n这道坑爹的题目条件居然要自己摸索！！！！然后就开始了痛苦的修bug之路…..（需求都不好好提，这样的甲方还是刷上面包糠带到河边吧）\n最后，我弄出了这样的DFA图\n其中，1 3 4 6 9 是可接受状态，0是初始状态~\n然后就快乐的跑起来咯~\nD是指数字，这个可以先转换一下再跑DFA，最后跑出了0ms的效果，也有可能LeetCode日常抽风~~\n代码放这咯~\n#include \u003cstring\u003e #include \u003cvector\u003e #include \u003calgorithm\u003e #include \u003cstack\u003e #include \u003cset\u003e #include \u003cmap\u003e #include \u003ciostream\u003e #define mn 20 using namespace std; class Solution { public: int zt[mn][mn]; string myinit(string s) { for (int i = 0; i \u003c mn; i++) for (int j = 0; j \u003c mn; j++) zt[i][j] = -1; zt[0][0] = 2; zt[0][5] = 7; zt[1][0] = 1; zt[1][2] = 1; zt[1][3] = 4; zt[1][5] = 6; zt[1][7] = 6; zt[1][10] = 4; zt[1][11] = 9; zt[2][1] = 5; zt[2][3] = 5; zt[2][4] = 5; zt[3][0] = 10; zt[3][1] = 3; zt[3][2] = 10; zt[4][0] = 0; zt[4][1] = 9; zt[4][3] = 9; zt[4][4] = 9; zt[4][6] = 9; zt[4][9] = 9; string str = \"\"; for (char c : s) { if (c \u003e= '0' \u0026\u0026 c \u003c= '9') { if (str.size() \u0026\u0026 str.back() == 'd') continue; else str.push_back('d'); } else str.push_back(c); } return str; } int zh(char c) { if (c == '-' || c == '+')return 0; if (c == 'd')return 1; if (c == 'e')return 2; if (c == '.')return 3; if (c == ' ' || c == '\\t' || c == '\\n') return 4; return 5; } bool isNumber(string s) { s = myinit(s); int now = 0; for (char c : s) { now = zt[zh(c)][now]; if (now == -1)return 0; } return (now == 1 || now == 3 || now == 4 || now == 6 || now == 9); } }; ","permalink":"/posts/tencent-2351644/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-08-12；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n今天刷了一下LeetCode，发现了一道很有感觉的题目，题面如下：\n","title":"LeetCode 65：有效数字 DFA自动机解法"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-08-12；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n倒霉透了，电脑每次都在关键时候给我整点刺激的，笔试做最后一题的时候，ide卡死了，怕关了笔试页面打不开又不敢重启，硬是靠着记事本+脑内编译做完了。。。。\n话说为啥华为阿里这些大企业笔试那么水，发offer却。。。。。\n今天的题目比较简单，第一题是让你开个咖啡店，每一杯咖啡5块钱，接下来进来一堆顾客，每个顾客都会拿5块或10块或20块的整钱支付，问你能不能找的开，初始状态你没有零钱~\n这题秒了~~不放代码了~\n第二题是个最基本的DFS，给你个地图问你从左上角到右下角是否可达，，也秒了~\n代码如下：\n#include \u003ciostream\u003e #include\u003cstdio.h\u003e #include\u003cstring\u003e #include \u003cvector\u003e #include \u003calgorithm\u003e #include \u003cstack\u003e #include \u003cset\u003e #include \u003cmap\u003e using namespace std; int a[100][100]; bool b[100][100]; int bc; int m, n; bool dfs(int i, int j) { if(i \u003c 0 || j \u003c 0)return 0; if (i \u003e= n || j \u003e= m)return 0; if (i == n - 1 \u0026\u0026 j == m - 1)return 1; if (b[i][j])return 0; b[i][j] = 1; if (!a[i][j])return 0; if (dfs(i - bc, j))return 1; if (dfs(i + bc, j))return 1; if (dfs(i, j - bc))return 1; if (dfs(i, j + bc))return 1; return 0; } int main() { scanf(\"%d%d%d\", \u0026bc,\u0026n,\u0026m); for (int i = 0; i \u003c n; i++) { for (int j = 0; j \u003c m; j++) { scanf(\"%d\", \u0026a[i][j]); } } if(dfs(0, 0))printf(\"1\\n\"); else printf(\"0\\n\"); } 第三题，爷的噩梦来了，题目倒是没啥，IDE抽风了，重启IDE也不能解决，，又不敢跳出去百度这问题咋解决，万一被判作弊就得不偿失，，也不敢直接重启，，万一这卷子直接给我提交了。。。。\n最后靠着记事本+脑内运行+oj自测，我终于ac了！！！\n题目也不难，就是个字符串按照一定规则重新排列的，给你个字符串比如abcdefghijklmn，再给你个数字比如5\n然后按照这个样子画一下：\na b\nc d\ne\nf g\nh i\nj k\nl\nm n\n然后竖下来读\n变成 ahcfjmeldgknbi\n难度不高代码如下：（我恨Visio Studio…..本来三题半小时多就能做完的，呜呜呜）\n#include \u003ciostream\u003e #include\u003cstdio.h\u003e #include\u003cstring\u003e #include \u003cvector\u003e #include \u003calgorithm\u003e #include \u003cstack\u003e #include \u003cset\u003e #include \u003cmap\u003e using namespace std; string str=\"\"; int main() { string strs; cin \u003e\u003e strs; int x=0; bool pd = 0; for (char c:strs) { if (c == ',') { pd = 1; continue; } if (!pd)str.push_back(c); else { x *= 10; x += c - '0'; } } int zq = 2 * x - 3; string ans=\"\"; for (int i = 0; i \u003c str.size(); i+=zq) { ans.push_back(str[i]); } for (int i = 1; i \u003c x / 2;i++) { for (int j = 2*i; j \u003c str.size(); j += zq) { ans.push_back(str[j]); int t = (x + 1) / 2 - i; t = 4 * t - 5 + j; if(t\u003c str.size())ans.push_back(str[t]); } } for(int i=x-1; i \u003c str.size(); i += zq) { ans.push_back(str[i]); } for (int i = x / 2-1; i \u003e 0; i--) { for (int j = 2 * i + 1; j \u003c str.size(); j += zq) { ans.push_back(str[j]); int t= (x + 1) / 2 - i; t = 4 * t - 5 + j; if (t \u003c str.size())ans.push_back(str[t]); } } for (int i = 1; i \u003c str.size(); i += zq) { ans.push_back(str[i]); } cout \u003c\u003c ans \u003c\u003c endl; } ","permalink":"/posts/tencent-2351642/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-08-12；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n倒霉透了，电脑每次都在关键时候给我整点刺激的，笔试做最后一题的时候，ide卡死了，怕关了笔试页面打不开又不敢重启，硬是靠着记事本+脑内编译做完了。。。。\n","title":"记华为笔试编程题目（软件开发）"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-08-10；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n听说华为会让人在LeetCode上手撕代码，我就去那瞄了一眼，随手点到了N皇后问题~\n这题目以前做过，不过今天突然想到了个新的思路，就是用位来存不可置放点，比如弄3个数z,y,isfill，初始状态都是0。\n当我在第0行放一个点时，比如放在第2列，这三个数就和1\u003c\u003c2做或操作，变成了00100000….（左边为最小位）\n当我在第1行的时候，z左移一位，y右移一位，变成z=010000000….，y=0001000000…，isfill不变，将它们三或一下，得到011100000…..这时候0的位子就可以放皇后，1的位置不能放直接剪枝~~所以第1 2 3列不能放，如果我们第1行放在第0列的话，\n它们三就变成z=11000000….，y=1001000000…，isfill==10100000….，之后的操作就都一样了~\n因为直接用int存的话n最大只能32位，所以我改成了数组，第j列就是[j/32]的j%32位，解决了存储的问题，算法就直接用回溯法就行了~~~\n（LeetCode的输出好蠢啊….呐喊.jpg\n代码如下：\n#include\u003ciostream\u003e #include \u003cstring\u003e #include \u003cvector\u003e #include \u003cstack\u003e using namespace std; class Solution { public: int isfill[10], z[10], y[10]; stack\u003cint\u003ext; stack\u003cint\u003e tt; int count = 0; vector\u003cvector\u003cstring\u003e\u003e ans; vector\u003cvector\u003cstring\u003e\u003e solveNQueens(int n) { for (int i = 0; i \u003c n; i++) { sol(0, i, n); } return ans; } int toz(int* a , int c, int n) { int ans = a[0] \u0026 1; for (int i = 0; i \u003c= n / 32; i++) { a[i] \u003e\u003e= 1; a[i] |= (a[i + 1] \u0026 1) \u003c\u003c 31; } if (c) { a[n / 32] |= 1 \u003c\u003c (n % 32); } return ans; } int toy(int* a, int c, int n) { int t,ans; t = n % 32; ans = a[n / 32] \u0026 (1\u003c\u003ct); a[n / 32] -= ans; for (int i = 0; i \u003c= n / 32; i++) { t = a[i] \u0026 (1 \u003c\u003c 31); a[i] \u003c\u003c= 1; if (c)a[i] |= 1; c = t; } return ans; } bool sol(int i, int j, int n) { if (i \u003e= n) { string str=\"\"; vector\u003cstring\u003e vect; while (!xt.empty()) { for (int c = 0; c \u003c n; c++) { if (c == xt.top())str .push_back('Q') ; else str.push_back('.'); } vect.push_back(str); str.clear(); tt.push(xt.top()); xt.pop(); } ans.push_back(vect); count++; while (!tt.empty()) { xt.push(tt.top()); tt.pop(); } return 1; } if (i == 0) { memset(isfill, 0, sizeof isfill); memset(z, 0, sizeof z); memset(y, 0, sizeof y); while (!xt.empty())xt.pop(); } int t = isfill[j / 32] | z[j / 32] | y[j / 32]; if (t \u0026 (1 \u003c\u003c (j % 32))) { return 0; } t = 1 \u003c\u003c (j % 32); isfill[j / 32] |= t; z[j / 32] |= t; y[j / 32] |= t; int tz = toz(z, 0, n); int ty = toy(y, 0, n); xt.push(j); for (int k = 0; k \u003c n; k++) { sol(i + 1, k, n); if (i + 1 \u003e= n)break; } toz(y, ty, n); toy(z, tz, n); isfill[j / 32] -= t; z[j / 32] -= t; y[j / 32] -= t; xt.pop(); return 0; } }; ","permalink":"/posts/tencent-2351620/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-08-10；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n听说华为会让人在LeetCode上手撕代码，我就去那瞄了一眼，随手点到了N皇后问题~\n","title":"N皇后问题--bitset解的思路"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-07-28；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n昨天晚上在线笔试了一下阿里巴巴JAVA研发岗的校招编程题，难度倒是不大，不过快两年没刷算法题的我差点没做完~~今天写个博客记录一下。 一共两道题，第一题题面大概是给一串单调递增数字，问如果a和b两个人轮流选择其中的一个数字，被选中的数字中最左边的那个和小于这个数字的数字全部删掉（比如[2 2 2 3 3 5]，若是选中了3 ，那么剩下数组为[3 5]），假设两人都很聪明问谁会获胜？ 解：这题难度不大，我们可以发现只要某个数字的数量为奇数个，那么先手胜利，否则后手胜利，读入遍历一遍就a了，本题就不给代码了。。 第二题稍微难点，题面是一个装小气鬼送礼物，他有一个架子，有n排礼物（每个礼物有自己的权值c，c越大礼物越贵），他要送m个礼物出去，但是他又不想送最贵的m个又想装大方，所以他给自己立了个规矩，每次选礼物都只能从任意一排的两端选择，问他最贵能送多少权值出去？ 输入格式：第一行输入n，m ，下面n行每行输入一个x代表这一行有x个礼物，后面跟x个数字，每个数字代表这个位置的礼物的权值。 输出：一个数字，代表最贵的权值。 解：这题需要转换一下，首先先把b[n][m]算出来，b[i][j]代表第i行选择j个礼物的最高权值。这里可以稍微做个优化，每个c[i][j]读入的时候都加上c[i][j-1]（我把c[i][0]用来存x了，所以j=1时候特判一下不加），c[i][j]就成了第i行前j个礼物的总权值，这时候算b[i][j]就简单一些了，遍历z\u003c=j，让y=j-z，c[i][j]=max（c[i][z] + c[i][zs] - c[i][zs - y]）,zs表示本行礼物数量，j\u003ezs的时候开始算下一行。 b[n][m]算好后，就可以开始用动态规划算答案了，转移方程dp[i][j]=max（dp[i-1][j-bc] + b[i][bc] , 0\u003cbc\u003c=j），由于本题的m比较大，不知道会不会mle，所以我这边把它优化成一维dp了，之后遍历一遍找到最大值就行了。 代码如下： #include \u003ciostream\u003e #include\u003cstdio.h\u003e int c[105][105]; int b[105][105]; int dp[10005]; int dpa[10005]; int main() { int n, m, x,ans; scanf(\"%d%d\", \u0026n, \u0026m); for (int i = 0; i \u003c n; i++) { scanf(\"%d\", \u0026c[i][0]); for (int j = 1; j \u003c= c[i][0]; j++) { scanf(\"%d\", \u0026x); c[i][j] = x; if (j \u003e 1)c[i][j] += c[i][j - 1]; } } for (int i = 0; i \u003c n; i++) { int zs = c[i][0]; for (int j = 1; j \u003c= zs; j++) { for (int z = 0; z \u003c= j; z++) { int y = j - z; if(z!=0)ans = c[i][z] + c[i][zs] - c[i][zs - y]; else ans = c[i][zs] - c[i][zs - y]; b[i][j] = b[i][j] \u003e ans ? b[i][j] : ans; } } } for (int i = 0; i \u003c n; i++) { ans = 0; for (int j = 0; j \u003c= m; j++) { for (int bc = 0; bc \u003c= j; bc++) { int temp = dp[j-bc] + b[i][bc]; if (temp \u003e ans)ans = temp; } dpa[j] = ans; } for (int j = 1; j \u003c= m; j++)dp[j] = dpa[j]; } ans = 0; for (int i = 0; i \u003c= m; i++)ans = ans \u003e dp[i] ? ans : dp[i]; printf(\"%d\\n\", ans); } ","permalink":"/posts/tencent-2351656/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-07-28；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n昨天晚上在线笔试了一下阿里巴巴JAVA研发岗的校招编程题，难度倒是不大，不过快两年没刷算法题的我差点没做完~~今天写个博客记录一下。 一共两道题，第一题题面大概是给一串单调递增数字，问如果a和b两个人轮流选择其中的一个数字，被选中的数字中最左边的那个和小于这个数字的数字全部删掉（比如[2 2 2 3 3 5]，若是选中了3 ，那么剩下数组为[3 5]），假设两人都很聪明问谁会获胜？ 解：这题难度不大，我们可以发现只要某个数字的数量为奇数个，那么先手胜利，否则后手胜利，读入遍历一遍就a了，本题就不给代码了。。 第二题稍微难点，题面是一个装小气鬼送礼物，他有一个架子，有n排礼物（每个礼物有自己的权值c，c越大礼物越贵），他要送m个礼物出去，但是他又不想送最贵的m个又想装大方，所以他给自己立了个规矩，每次选礼物都只能从任意一排的两端选择，问他最贵能送多少权值出去？ 输入格式：第一行输入n，m ，下面n行每行输入一个x代表这一行有x个礼物，后面跟x个数字，每个数字代表这个位置的礼物的权值。 输出：一个数字，代表最贵的权值。 解：这题需要转换一下，首先先把b[n][m]算出来，b[i][j]代表第i行选择j个礼物的最高权值。这里可以稍微做个优化，每个c[i][j]读入的时候都加上c[i][j-1]（我把c[i][0]用来存x了，所以j=1时候特判一下不加），c[i][j]就成了第i行前j个礼物的总权值，这时候算b[i][j]就简单一些了，遍历z\u003c=j，让y=j-z，c[i][j]=max（c[i][z] + c[i][zs] - c[i][zs - y]）,zs表示本行礼物数量，j\u003ezs的时候开始算下一行。 b[n][m]算好后，就可以开始用动态规划算答案了，转移方程dp[i][j]=max（dp[i-1][j-bc] + b[i][bc] , 0\u003cbc\u003c=j），由于本题的m比较大，不知道会不会mle，所以我这边把它优化成一维dp了，之后遍历一遍找到最大值就行了。 代码如下： #include \u003ciostream\u003e #include\u003cstdio.h\u003e int c[105][105]; int b[105][105]; int dp[10005]; int dpa[10005]; int main() { int n, m, x,ans; scanf(\"%d%d\", \u0026n, \u0026m); for (int i = 0; i \u003c n; i++) { scanf(\"%d\", \u0026c[i][0]); for (int j = 1; j \u003c= c[i][0]; j++) { scanf(\"%d\", \u0026x); c[i][j] = x; if (j \u003e 1)c[i][j] += c[i][j - 1]; } } for (int i = 0; i \u003c n; i++) { int zs = c[i][0]; for (int j = 1; j \u003c= zs; j++) { for (int z = 0; z \u003c= j; z++) { int y = j - z; if(z!=0)ans = c[i][z] + c[i][zs] - c[i][zs - y]; else ans = c[i][zs] - c[i][zs - y]; b[i][j] = b[i][j] \u003e ans ? b[i][j] : ans; } } } for (int i = 0; i \u003c n; i++) { ans = 0; for (int j = 0; j \u003c= m; j++) { for (int bc = 0; bc \u003c= j; bc++) { int temp = dp[j-bc] + b[i][bc]; if (temp \u003e ans)ans = temp; } dpa[j] = ans; } for (int j = 1; j \u003c= m; j++)dp[j] = dpa[j]; } ans = 0; for (int i = 0; i \u003c= m; i++)ans = ans \u003e dp[i] ? ans : dp[i]; printf(\"%d\\n\", ans); } ","title":"记阿里笔试编程题目（JAVA研发）"},{"content":" 旧作补记\n原始时间：2020-05-05\n本站补录：2026-10-12。\n天穹之上，日月盈亏，星辰浩瀚。\n忆望舒姣姣，嫦娥有悔；银汉邈邈，鹊桥难渡。\n文昌古城，长征新发，箭指苍穹问天高。\n待明年，看荧惑妖娆，重整天宫。\n星河皓月当空，怎不惹世代人神往？\n趁科技兴国，万邦安宁，星辰大海，今人逐之。\n神州崛起，蓝星叱咤，历史沉浮俱往矣。\n今非昔，举头望九天，未来家国。\n","permalink":"/posts/pangwu/","summary":" 旧作补记\n原始时间：2020-05-05\n本站补录：2026-10-12。\n天穹之上，日月盈亏，星辰浩瀚。\n忆望舒姣姣，嫦娥有悔；银汉邈邈，鹊桥难渡。\n文昌古城，长征新发，箭指苍穹问天高。\n待明年，看荧惑妖娆，重整天宫。\n","title":"沁园春·胖五"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-04-30；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n隐私处理： 示例中的个人化用户名和固定密码已替换为占位符。\n创建用户：\ncreate user 'demo_user'@'%' identified by 'REPLACE_WITH_STRONG_PASSWORD'; 赋予权限：\ngrant all privileges on xxxDB.* to 'demo_user'@'%' ; 删除用户：\ndrop user 'demo_user'@'%' ; 操作后记得刷新权限表：\nflush privileges; 查看用户信息：\nselect distinct * from (select user.Host,user.User,db.Db,user.Password,user.Drop_priv,user.Grant_priv,user.Alter_priv from db inner JOIN user on db.user=user.user) as it where user='demo_user'; ","permalink":"/posts/tencent-2351621/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-04-30；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n隐私处理： 示例中的个人化用户名和固定密码已替换为占位符。\n创建用户：\ncreate user 'demo_user'@'%' identified by 'REPLACE_WITH_STRONG_PASSWORD'; 赋予权限：\n","title":"MySql创建新用户并授权"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-04-17；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n增加swap分区 查看内存和swap分区大小命令：\nfree -m 创建swap文件（大小为bs*count的Swap空间），目录为/myswap：\ncd / sudo mkdir myswap cd myswap sudo dd if=/dev/zero of=swapfile bs=1M count=2048 sudo mkswap swapfile 激活swap文件：\nsudo swapon swapfile 设置该swap开机自动挂载：\nsudo cp /etc/fstab /etc/fstab.bf echo '/myswap/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab 删除某swap分区： 停用该swap分区文件：\nsudo swapoff -v /swapfile 删除/etc/fstab中对应的行：\n//自己用vi或者vim删除\n删除swapfile文件：\nsudo rm /swap/swapfile ","permalink":"/posts/tencent-2351623/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-04-17；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n增加swap分区 查看内存和swap分区大小命令：\nfree -m 创建swap文件（大小为bs*count的Swap空间），目录为/myswap：\n","title":"Ubuntu18.04 增加Swap分区"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-02-20；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n大一时候写过不科学计算器，那时候碰到好多问题都是头铁莽上去，加特判就完事了，导致屁大点功能写了几百行代码，一点也不优雅（雾）。\n今天晚上闲来无事就把那份代码重写了一下，用算数栈的方法，顺便把double改成了大数BigDecimal，妈妈再也不用怕我溢出了~~\n具体代码在gitee上 ：https://gitee.com/mofanyunxiang/calculator\n核心代码不多，都在下面了：\nfor(int i=0;i\u003cs.length();i++) { char si=s.charAt(i); if(si==')') { char tc='\\0'; while(!cstack.empty())//将括号内部的东西算完，直到碰到左括号后匹配 { tc=cstack.pop().toString().charAt(0); if(tc=='(')break; BigDecimal n2=nstack.pop(); BigDecimal n1=nstack.pop(); n1=add(n1,n2,tc); if(n1==null)return \"null\"; nstack.push(n1); } if(tc!='(')return \"Bracket mismatch\"; //括号不匹配，如果缺右括号的话，默认自动在算式尾部添加，缺左括号则报错 continue; } ndj=dj; dj=zh(si); if(dj==-1||((ndj!=-1\u0026\u0026si=='-')))//数字为-1，或者带负号的数字，负号和减号判断规则见上 { int be=i++; while(i\u003cs.length()\u0026\u0026zh(s.charAt(i))\u003c0)i++; BigDecimal n1=new BigDecimal(s.substring(be,i)); i--; nstack.push(n1); continue; } if(dj==0)//左括号 { cstack.push(si); continue; } if(dj\u003e=1)//算符计算 { if(cstack.empty()) { cstack.push(si); continue; } char tc=cstack.peek().toString().charAt(0); while(zh(tc)\u003e=dj)//入栈时如果本算符优先级小于等于栈顶算符，则先计算栈顶算符 { BigDecimal n2=nstack.pop(); BigDecimal n1=nstack.pop(); cstack.pop(); n1=add(n1,n2,tc); if(n1==null)return \"null\"; nstack.push(n1); if(!cstack.empty())tc=cstack.peek().toString().charAt(0); else tc='0'; } cstack.push(si);//算符入栈 } } /** * 算式栈全部计算，此时优先级就是出栈顺序，直接计算即可。 */ while(!cstack.empty()) { char tc=cstack.pop().toString().charAt(0); if(tc=='(')continue; BigDecimal n2=nstack.pop(); BigDecimal n1=nstack.pop(); n1=add(n1,n2,tc); if(n1==null)return \"null\"; nstack.push(n1); } s=nstack.pop().toString(); ","permalink":"/posts/tencent-2351625/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-02-20；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n大一时候写过不科学计算器，那时候碰到好多问题都是头铁莽上去，加特判就完事了，导致屁大点功能写了几百行代码，一点也不优雅（雾）。\n","title":"带括号计算器----Java实现"},{"content":" 旧文迁移 · 补录说明\n原始发表：2020-02-10；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n隐私处理： 数据库名和服务地址已替换为示例值；原数据库状态截图改为脱敏文本。\n在主从机上的配置文件中都修改[mysqld]下的内容\nserver-id = 1 （主1 从2）\nlog-bin = mysql-bin\nbinlog-do-db = example_db binlog-ignore-db = mysql 重启主从机的mysql\n主机用show master status；查询File和Position的数据记下；\n在从机上输入如下指令\nchange master to master_host=‘db.example.com’, (你的域名或者ip)\nmaster_user=‘root’, (你的用户)\nmaster_password=’*******’, (你的密码)\nmaster_port=3306, (你的端口)\nmaster_log_file=‘localhost-bin.000003’, (刚刚记下的File)\nmaster_log_pos=110983 ;(刚刚记下的Position)\n括号（包括里面）全删掉\n然后\nstart slave; show slave status\\G 若是如下图有两个yes即可\nSlave_IO_Running: Yes Slave_SQL_Running: Yes （迁移说明：原状态截图含具体库名，此处只保留正文需要说明的两个状态值。）\n同步出现问题参考：https://www.cnblogs.com/l-hh/p/9922548.html\n","permalink":"/posts/tencent-2351627/","summary":" 旧文迁移 · 补录说明\n原始发表：2020-02-10；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n隐私处理： 数据库名和服务地址已替换为示例值；原数据库状态截图改为脱敏文本。\n在主从机上的配置文件中都修改[mysqld]下的内容\nserver-id = 1 （主1 从2）\nlog-bin = mysql-bin\nbinlog-do-db = example_db binlog-ignore-db = mysql ","title":"MySql双机热备份"},{"content":" 旧文迁移 · 补录说明\n原始发表：2019-09-07；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n思路很简单\n如果蛇能吃到食物且吃到食物后能找到尾巴，就去吃食物（最短路）\n否则就跟着尾巴走（最长路）\n期间做了一点优化，比如把步子存起来就不用跑一步看一下了，最长路也不需要，只要不是最短路就行了\n代码上传gitee了：https://gitee.com/linhui_fun/gluttonous_snake\n试着跑了一下，视频在B站\n核心代码\npublic void AImove() { if (mnsnake == null) mnsnake = new ArrayDeque\u003c\u003e(); else mnsnake.clear(); for (int i = 0; i \u003c height; i++) { for (int j = 0; j \u003c width; j++) { btable[i][j] = -table[i][j]; } } if (canGet(snake.getFirst(), food, btable, mnsnake) \u003e 0) { for (int i = 0; i \u003c height; i++) { for (int j = 0; j \u003c width; j++) { btable[i][j] = -table[i][j]; } } for (Body_joint body_joint : mnsnake) { btable[body_joint.getX()][body_joint.getY()] = -1; } if (canGet(food, snake.getLast(), btable, null) \u003e 0) { return; } } mnsnake.clear(); int maxx = 0; for (int i = -1; i \u003c= 1; i++) { for (int j = -1; j \u003c= 1; j++) { if (i == j || i * j != 0 || isOut(snake.getFirst().getX() + i, snake.getFirst().getY() + j)) continue; if (table[snake.getFirst().getX() + i][snake.getFirst().getY() + j] != 0) continue; for (int ii = 0; ii \u003c height; ii++) { for (int jj = 0; jj \u003c width; jj++) { btable[ii][jj] = -table[ii][jj]; } } btable[snake.getLast().getX()][snake.getLast().getY()] = 0; int t = canGet(new Body_joint(snake.getFirst().getX() + i, snake.getFirst().getY() + j), snake.getLast(), btable, null); t+=snake.getFirst().getDis(food); if (maxx \u003c t) { maxx = t; if (mnsnake.isEmpty()) mnsnake.addFirst(new Body_joint(snake.getFirst().getX(), snake.getFirst().getY())); mnsnake.getFirst().setDx(i); mnsnake.getFirst().setDy(j); } } } if(mnsnake.isEmpty()) { mnsnake.addFirst(new Body_joint(snake.getFirst())); mnsnake.getFirst().setDx(snake.getLast().getX()-snake.getFirst().getX()); mnsnake.getFirst().setDy(snake.getLast().getY()-snake.getFirst().getY()); } } public int canGet(Body_joint qd, Body_joint zd, int[][] map, Deque\u003cBody_joint\u003e road) { map[qd.getX()][qd.getY()] = map[zd.getX()][zd.getY()] = 0; PriorityQueue\u003cBody_joint\u003e openlist = new PriorityQueue\u003c\u003e(); int[][] oc = new int[height][width]; int[][] ddx = new int[height][width]; int[][] ddy = new int[height][width]; for (int i = 0; i \u003c height; i++) { for (int j = 0; j \u003c width; j++) { ddx[i][j] = ddy[i][j] = oc[i][j] = 0; } } qd.setS(1 + qd.getDis(zd)); openlist.add(qd); oc[qd.getX()][qd.getY()] = 1;//1是open -1是close map[qd.getX()][qd.getY()] = 1; Body_joint btemp, bbtemp = null; while (!openlist.isEmpty()) { if (map[zd.getX()][zd.getY()] != 0) break; btemp = openlist.poll(); if (oc[btemp.getX()][btemp.getY()] == -1) continue; oc[btemp.getX()][btemp.getY()] = -1; for (int i = -1; i \u003c= 1; i++) { for (int j = -1; j \u003c= 1; j++) { if (i == j || i * j != 0 || isOut(btemp.getX() + i, btemp.getY() + j) || oc[btemp.getX() + i][btemp.getY() + j] == -1 || map[btemp.getX() + i][btemp.getY() + j] == -1) continue;//不可抵达或close if (oc[btemp.getX() + i][btemp.getY() + j] == 0||map[btemp.getX() + i][btemp.getY() + j] \u003e map[btemp.getX()][btemp.getY()] + 1) { oc[btemp.getX() + i][btemp.getY() + j] = 1; map[btemp.getX() + i][btemp.getY() + j] = map[btemp.getX()][btemp.getY()] + 1; bbtemp = new Body_joint(btemp.getX() + i, btemp.getY() + j, i, j); bbtemp.setS(map[btemp.getX() + i][btemp.getY() + j] + bbtemp.getDis(zd)); openlist.add(bbtemp); ddx[bbtemp.getX()][bbtemp.getY()] = i; ddy[bbtemp.getX()][bbtemp.getY()] = j; } } } } if (map[zd.getX()][zd.getY()] == 0) return -1; ; if (road == null) return map[zd.getX()][zd.getY()]; int tx = zd.getX(), ty = zd.getY(); road.addFirst(new Body_joint(tx, ty)); while (road.getFirst().getX() != qd.getX() || road.getFirst().getY() != qd.getY()) { bbtemp = new Body_joint(tx - ddx[tx][ty], ty - ddy[tx][ty]); bbtemp.setDx(ddx[road.getFirst().getX()][road.getFirst().getY()]); bbtemp.setDy(ddy[road.getFirst().getX()][road.getFirst().getY()]); road.addFirst(bbtemp); int tempx=tx; int tempy=ty; tx -= ddx[tempx][tempy]; ty -= ddy[tempx][tempy]; } road.pollLast(); return road.size(); } ","permalink":"/posts/tencent-2351614/","summary":" 旧文迁移 · 补录说明\n原始发表：2019-09-07；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n思路很简单\n如果蛇能吃到食物且吃到食物后能找到尾巴，就去吃食物（最短路）\n否则就跟着尾巴走（最长路）\n","title":"用A*算法让贪吃蛇跑满屏幕（顺手写了个贪吃蛇验证）"},{"content":" 旧文迁移 · 补录说明\n原始发表：2019-08-20；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n隐私处理： 本地用户目录已替换为示例路径。\n在字符画的基础上增加了播放功能\n思路很简单，javacv抽帧，逐帧生成对应字符画后保存，生成完通过swing播放。\n视频样例：https://www.bilibili.com/video/av64526993/\n开源地址：https://gitee.com/mofanyunxiang/zifuhua_M\nFlash.java\npackage video; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Array; import java.util.Arrays; public class Flash implements Serializable { transient BufferedImage image; String asciiImage; public Flash(BufferedImage image) { this.image=image; this.asciiImage=createAsciiPic(image); } public static String createAsciiPic(BufferedImage image) { image=compressImage(image); final String base = \"@B%8\u0026WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\\\|()1{}[]?-_+~\u003c\u003ei!lI;:,\\\"^`'. \";// 字符串由复杂到简单 String ans = \"\"; int sl=0; for (int y = 0; y \u003c image.getHeight(); y +=2) { for (int x = 0; x \u003c image.getWidth(); x++) { final int pixel = image.getRGB(x, y); final int r = (pixel \u0026 0xff0000) \u003e\u003e 16, g = (pixel \u0026 0xff00) \u003e\u003e 8, b = pixel \u0026 0xff; final float gray = 0.299f * r + 0.578f * g + 0.114f * b; int index = (int)(gray * (base.length() + 1) / 256); ans += index \u003e= base.length() ? \" \" : String.valueOf(base.charAt(index)); } ans += \"\\n\"; } return ans; } public static BufferedImage compressImage(BufferedImage srcImg){ int h = srcImg.getHeight(); int w = srcImg.getWidth(); int size=400; if(Math.max(h,w)\u003c=size) return srcImg; int new_H; int new_W; if(w\u003eh){ new_W = size; new_H = size*h/w ; }else{ new_H = size; new_W = size*w/h; } BufferedImage smallImg = new BufferedImage(new_W,new_H,srcImg.getType()); Graphics g = smallImg.getGraphics(); g.drawImage(srcImg,0,0,new_W,new_H,null); g.dispose(); return smallImg; } } MyFrame.java\npackage video; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Date; import java.util.List; import javafx.scene.media.AudioClip; public class MyFrame extends JFrame implements Runnable { // 取得屏幕的宽度 int pwidth = Toolkit.getDefaultToolkit().getScreenSize().width; // 取得屏幕的高度 int pheight = Toolkit.getDefaultToolkit().getScreenSize().height; int height; int width; Video video; Thread t = new Thread(this); long st; int zs; AudioClip ac; public MyFrame(Video video,AudioClip ac,String filepath) { zs=0; this.video = video; width=1700; height=1000; this.setTitle(\"洛天依是我老婆~\"); // 设置窗体大小 this.setSize(width, height); // 设置窗体出现位置 this.setLocation((pwidth - width) / 2, (pheight - height) / 2); // 将窗体设置为大小不可改变 this.setResizable(false); // 将窗体的关闭方式设置为默认关闭后程序结束 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setBackground(Color.BLACK); this.setVisible(true); this.ac=ac; st=0; try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } t.start(); // 刷新屏幕,防止开始游戏时出现无法显示的情况. this.repaint(); } /*** * 使用了双缓冲技术防止屏幕闪烁 * @param g */ public void paint(Graphics g) { if(st\u003c=0) { if(ac!=null)ac.play(); st=new Date().getTime(); } zs=(int)((new Date().getTime()-st)*video.rate/1000); if(zs\u003e=video.ftp-5)return; String str=video.flashs.get(zs).asciiImage; BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g2 = bi.createGraphics(); g2.setColor(Color.BLACK); g2.fillRect(0, 0, width, width); g2.setColor(Color.WHITE); int wsize= (width-60)/video.wsize+4; g2.setFont(new Font(\"宋体\", Font.BOLD,wsize)); String[] strs=str.split(\"\\n\"); int i=0; for(String lstr:strs) { g2.drawString(lstr,40,90+i*wsize); i++; } g.drawImage(bi, 0, 0, this); } public void run() { // TODO Auto-generated method stub while (true) { this.repaint(); try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } } } Video.java\npackage video; import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter; import java.awt.image.BufferedImage; import java.io.*; import java.util.ArrayList; import java.util.Date; import java.util.List; import javafx.scene.media.AudioClip; public class Video implements Serializable { private static final long serialVersionUID= 3960073332674427191l; double duration; double rate; int ftp; int finterval; int height; int width; int wsize; File videoFile; List\u003cFlash\u003e flashs; String fileSavePath=\"D:/save/\"; int flag; public Video(File file) { videoFile=file; flashs=new ArrayList\u003cFlash\u003e(); flag=0; init(file.getPath()); } public Video(String filepath) { try { ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filepath)); Video video=(Video) ois.readObject(); this.duration=video.duration; this.rate=video.rate; this.ftp=video.ftp; this.finterval=video.finterval; this.height=video.height; this.width=video.width; this.wsize=video.wsize; this.videoFile=video.videoFile; this.flashs=video.flashs; this.flag=video.flag; if(flag\u003cftp)init(filepath); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public void save(String filepath) { try { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(filepath+new Date().getTime()+\".video\")); oos.writeObject(this); oos.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void init(String filepath) { Frame frame = null; FFmpegFrameGrabber fFmpegFrameGrabber = new FFmpegFrameGrabber(videoFile); try { fFmpegFrameGrabber.start(); ftp = fFmpegFrameGrabber.getLengthInFrames(); rate=fFmpegFrameGrabber.getFrameRate(); duration=ftp / rate; finterval=(int)(1000 /rate +0.5); System.out.println(\"时长 \" + ftp / rate / 60); System.out.println(\"开始运行视频提取帧，耗时较长\"); frame = fFmpegFrameGrabber.grabImage(); height=FrameToBufferedImage(frame).getHeight(); width=FrameToBufferedImage(frame).getWidth(); wsize=Flash.compressImage(FrameToBufferedImage(frame)).getWidth(); //ftp=40; while (flag \u003c ftp) { //获取帧 frame = fFmpegFrameGrabber.grabImage(); if(flag\u003cflashs.size())continue; if(flag/1000\u003c(flag+1)/1000)this.save(filepath); // System.out.println(frame); if (frame != null) { flashs.add(new Flash(FrameToBufferedImage(frame))); flag++; } else { ftp--; } System.out.println(flag+\"/\"+ftp); System.gc(); } System.out.println(\"============运行结束============\"); fFmpegFrameGrabber.stop(); this.save(filepath); } catch (IOException E) { E.printStackTrace(); } } public static BufferedImage FrameToBufferedImage(Frame frame) { //创建BufferedImage对象 Java2DFrameConverter converter = new Java2DFrameConverter(); BufferedImage bufferedImage = converter.getBufferedImage(frame); return bufferedImage; } } Main.java\nimport javafx.scene.media.AudioClip; import video.MyFrame; import video.Video; import java.io.File; public class Main { public static void main(String[] args) { String filepath; /** * 输入生成的video文件的路径或者个视频的路径 * video是已完成渲染的视频，可立即启动 * 其它视频需生成video文件后可启动 */ filepath = \"C:\\\\Users\\\\demo-user\\\\Desktop\\\\a.video\"; Video video; if (filepath.substring(filepath.lastIndexOf(\".\") + 1).equals(\"video\")) { video = new Video(filepath); } else { video = new Video(new File(filepath)); video.save(filepath.substring(0, filepath.lastIndexOf(\".\"))); } new MyFrame(video, new AudioClip(new File(\"C:\\\\Users\\\\demo-user\\\\Desktop\\\\a.mp3\").toURI().toString())//该路径是播放音乐时的背景音乐的路径 , filepath.substring(0, filepath.lastIndexOf(\"\\\\\") + 1)); } } //C:\\Users\\demo-user\\Desktop\\a.mp4 ","permalink":"/posts/tencent-2351615/","summary":" 旧文迁移 · 补录说明\n原始发表：2019-08-20；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n隐私处理： 本地用户目录已替换为示例路径。\n在字符画的基础上增加了播放功能\n思路很简单，javacv抽帧，逐帧生成对应字符画后保存，生成完通过swing播放。\n","title":"字符画视频 动态字符画java源代码"},{"content":" 旧文迁移 · 补录说明\n原始发表：2019-07-20；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n在家无聊写了这个小项目，实现了账号注册，登录模块，以及签到模块。\n用到了jsp+mysql。\n账号注册和登录是直接从上学期jsp课设作业中扒出来的，稍微改了点东西，主要是数据方面的，账户存name + password + regdate + runningdays + maxrunningdays这几个数据。\n签到模块实现比较简单，数据为name+signindate。\n本来以为一个中午就能搞完，结果前端的东西和一些签到逻辑的细节零零散散一共还是花了几小时（不了解js的后果）。\n日历显示是网上随便抄了一份改了改就用了（链接：https://blog.csdn.net/qq_22317389/article/details/80349497）。\n代码在gitee上：https://gitee.com/linhui_fun/Check-in_System\n实例在：http://jlqd.linhui.fun\n效果大概是这样子的。\n还没签到是这个样子的\n这个系统以后就用来记录起床时间了~\n","permalink":"/posts/tencent-2351618/","summary":" 旧文迁移 · 补录说明\n原始发表：2019-07-20；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n在家无聊写了这个小项目，实现了账号注册，登录模块，以及签到模块。\n用到了jsp+mysql。\n","title":"java web小项目：记录签到系统"},{"content":" 旧文迁移 · 补录说明\n原始发表：2019-05-06；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n公共最长（连续）子串 ** 思路很简单，动态规划就行了，设dp[i][j]为a串的0-i，b串的0-j的最长公共后缀长度。那么状态转移方程就是dp[i][j]=a[i]==b[i]?dp[i-1][j-1]:0** 因为在i j为0时可能会出现dp[-1][-1]的情况，所以把dp数组后推一位，且dp数组会用到的最多就是dp[i-1][j-1],所以可以优化一下空间，把它变成一维数组从后往前dp，代码如下： #include \u003cbits/stdc++.h\u003e using namespace std; char a[10000007],b[10000007]; int dp[10000007]; int main() { while(1) { scanf(\"%s%s\",a,b); int acd=strlen(a);//计算a和b字符串长度 int bcd=strlen(b); int maxn=0,jl;//maxn用来记录最长后缀，jl记录maxn出现时公共字符串位置。 memset(dp,0,(sizeof (int))*(bcd+1));//把dp置0，初始化。 for(int i=0;i\u003cacd;i++) { for(int j=bcd-1;j\u003e=0;j--) { dp[j+1]=a[i]==b[j]?dp[j]+1:0;//核心代码，状态转移。 if(maxn\u003cdp[j+1]) { maxn=dp[j+1];//更新maxn与jl jl=i-maxn+1; } } } printf(\"%d\\n\",maxn);//输出 for(int i=jl;i\u003cjl+maxn;i++) { printf(\"%c\",a[i]); } printf(\"\\n\"); } } ","permalink":"/posts/tencent-2351619/","summary":" 旧文迁移 · 补录说明\n原始发表：2019-05-06；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n公共最长（连续）子串 ** 思路很简单，动态规划就行了，设dp[i][j]为a串的0-i，b串的0-j的最长公共后缀长度。那么状态转移方程就是dp[i][j]=a[i]==b[i]?dp[i-1][j-1]:0** 因为在i j为0时可能会出现dp[-1][-1]的情况，所以把dp数组后推一位，且dp数组会用到的最多就是dp[i-1][j-1],所以可以优化一下空间，把它变成一维数组从后往前dp，代码如下： #include \u003cbits/stdc++.h\u003e using namespace std; char a[10000007],b[10000007]; int dp[10000007]; int main() { while(1) { scanf(\"%s%s\",a,b); int acd=strlen(a);//计算a和b字符串长度 int bcd=strlen(b); int maxn=0,jl;//maxn用来记录最长后缀，jl记录maxn出现时公共字符串位置。 memset(dp,0,(sizeof (int))*(bcd+1));//把dp置0，初始化。 for(int i=0;i\u003cacd;i++) { for(int j=bcd-1;j\u003e=0;j--) { dp[j+1]=a[i]==b[j]?dp[j]+1:0;//核心代码，状态转移。 if(maxn\u003cdp[j+1]) { maxn=dp[j+1];//更新maxn与jl jl=i-maxn+1; } } } printf(\"%d\\n\",maxn);//输出 for(int i=jl;i\u003cjl+maxn;i++) { printf(\"%c\",a[i]); } printf(\"\\n\"); } } ","title":"公共最长（连续）子串"},{"content":" 旧文迁移 · 补录说明\n原始发表：2019-04-29；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n浮点数加减法过程模拟（C语言） 过程在课本（计算机组成与系统结构第五版）51页，不介绍了，代码如下（自己敲的不知道好使不，试了几次感觉还行）。 代码如下：（float精度好低）\n#include\u003cbits/stdc++.h\u003e using namespace std; void print_bin(int n)//输出n（int型）的二进制表示，用于调试 { int l = sizeof(n)*8;//总位数。 int i; if(i == 0) { printf(\"0\"); return; } for(i = l-1; i \u003e= 0; i --)//略去高位0. { if(n\u0026(1\u003c\u003ci)) break; } for(int k=1;k\u003c32-i;k++)printf(\"0\"); for(;i\u003e=0; i --) printf(\"%d\", (n\u0026(1\u003c\u003ci)) != 0); printf(\"\\n\"); } bool check_0(int n) { n\u0026=0x7fffffff;//去符号 if(n\u003c\u003c9)return 0;//尾数不为0 return n\u003e\u003e23==0x0;//阶码为00000000 } float calculate(float a,float b,char sign)//a sign b { float ans_f=0; int \u0026ans=*((int*)\u0026ans_f); int \u0026x=*((int*)\u0026a); int \u0026y=*((int*)\u0026b); //如果是减法就把y的第一位取反 if(sign=='-') { if(y\u00260x80000000)//如果y的第一位是1的话把它变0 { y\u0026=0x7fffffff; } else //把它变1 { y|=0x80000000; } } //0操作数检查 if(check_0(x))return b; if(check_0(y))return a; //对阶操作 int x_j=(x\u00260x7fffffff)\u003e\u003e23;//取阶码 int y_j=(y\u00260x7fffffff)\u003e\u003e23; int x_w=(x\u003c\u003c9)\u003e\u003e1;//取尾数 x_w=x_w\u00260x7fffffff; x_w\u003e\u003e=8; int y_w=(y\u003c\u003c9)\u003e\u003e1; y_w=y_w\u00260x7fffffff; y_w\u003e\u003e=8; x_w|=0x800000;//对尾数加上前置的1. y_w|=0x800000; while(x_j\u003cy_j) { ++x_j; x_w\u003e\u003e=1; if(x_w==0)return b; } while(x_j\u003ey_j) { ++y_j; y_w\u003e\u003e=1; if(y_w==0)return a; } int ans_j=x_j; if(x\u00260x80000000)x_w=-x_w;//如果是负数，尾数取负 if(y\u00260x80000000)y_w=-y_w; int ans_w=x_w+y_w; if(ans_w\u003c0)//负数则ans取负 { ans=1\u003c\u003c31; ans_w=-ans_w; } if(!ans_w)return 0;//如果ans为0，返回0 while(ans_w\u003e\u003e24)//处理尾数上溢 { ++ans_j; ans_w\u003e\u003e=1; if(ans_j\u003e\u003e8) { ans+=0x7fffffff;//阶码上溢，ans取最大值返回 return ans_f; } } while(!(ans_w\u003e\u003e23)) { --ans_j; ans_w\u003c\u003c=1; if(!ans_j) { ans=0;//阶码下溢，ans取0返回 return ans_f; } } ans_w\u0026=0xff7fffff; ans+=ans_w; ans+=ans_j\u003c\u003c23; return ans_f; } int main() { float aa,bb; char cc; while(1) { cin\u003e\u003eaa\u003e\u003ecc\u003e\u003ebb; cout\u003c\u003ccalculate(aa,bb,cc)\u003c\u003cendl; } } ","permalink":"/posts/tencent-2351610/","summary":" 旧文迁移 · 补录说明\n原始发表：2019-04-29；本站迁移：2026-10-12。正文与代码按历史版本归档。\n腾讯云原文 · 原个人博客地址\n浮点数加减法过程模拟（C语言） 过程在课本（计算机组成与系统结构第五版）51页，不介绍了，代码如下（自己敲的不知道好使不，试了几次感觉还行）。 代码如下：（float精度好低）\n","title":"浮点数加减法过程模拟（C语言）"},{"content":" 旧作补记\n原始时间：约 2019 年秋，具体日期不详\n本站补录：2026-10-12。\n南国秋景，层林尽染，碧草渐黄。\n看天降暮色，银汉邈邈；云疏漏影，圆月皎皎。\n山高水长，树影阑珊，鲤鱼池中乱点红。\n放眼去，见梧桐簌簌，独无人影。\n江山如此好景，问芸芸众生何时赏？\n惜建国七十，资本当道；码声久久，兄弟迟归。\n一代商才，杰克马云，韭菜言论坏人设。\n待我辈，把日月颠倒，以正乾坤。\n","permalink":"/posts/manong/","summary":" 旧作补记\n原始时间：约 2019 年秋，具体日期不详\n本站补录：2026-10-12。\n南国秋景，层林尽染，碧草渐黄。\n看天降暮色，银汉邈邈；云疏漏影，圆月皎皎。\n山高水长，树影阑珊，鲤鱼池中乱点红。\n放眼去，见梧桐簌簌，独无人影。\n","title":"沁园春·码农"},{"content":" 旧作补记\n原始时间：约 2018 年，具体月日不详\n本站补录：2026-10-12。\n权者愚庸至列强作乱\n毁国门戮我庶黎侵我河山\n伏尸遮蔽了万里国疆\n落寞 只留过往\n烽火狼烟迫书生戎装\n回首望河山染血心肠寸断\n何时可许我河清海晏\n无望 唯有征战\n国难当前 江山越陷\n醉心敌防 不入情欢\n悍捷台庄 笑声疏狂\n初心未忘 愿得国强\n布局解敌千里外战局推算\n喜交莫逆知己托赠勃朗\n同心杀贼不计政体红与黄\n然外敌尚垮 兄弟阋墙\n竟无奈兵刀相向\n枯草褪去了不遮土黄\n秋风凉静听鸦声绕树三匝\n书生当年意气剩了几行\n怀土 黯然渡海\n一身抱负又恰逢时间\n一步错过往辉煌悉数成幻\n一朝纠缠难分辨恩怨\n空余 一人独往\n阳光微凉 秋夕微凉\n旧事难忘 对错何妨\n系党争权 同胞相残\n遍地狼烟 满目苍黄\n江山迭代史册浮沉若惚恍\n报国一生落得如此凄惨\n谁知成败敲定了黑白是非\n若时光回转 初心未忘\n功过必重载史卷\n多年之后 我又梦到那天\n两鬓霜繁 腐旧征袍血染\n若当来生太平战火不犯边\n故园河山不再隔海相望\n锦绣流年盛世向岁月吊唁\n坐明朗堂前 笔墨挥散\n书生何须着戎装\n权者愚庸至列强作乱\n国殇莫遗忘\n","permalink":"/posts/chuyunfei/","summary":" 旧作补记\n原始时间：约 2018 年，具体月日不详\n本站补录：2026-10-12。\n权者愚庸至列强作乱\n毁国门戮我庶黎侵我河山\n伏尸遮蔽了万里国疆\n落寞 只留过往\n烽火狼烟迫书生戎装\n回首望河山染血心肠寸断\n何时可许我河清海晏\n无望 唯有征战\n","title":"锦鲤抄填词：亮剑·楚云飞"}]