[{"content":"Robocopy 快速复制大批量文件的技巧 Robocopy（Robust File Copy）是 Windows 内置的强大复制工具，下面从核心参数、提速技巧到完整示例进行说明。\n一、核心提速参数 参数 作用 说明 /R:0 重试次数设为 0 默认重试 100 万次，失败文件会严重拖慢速度，设 0 跳过 /W:0 重试等待秒数设为 0 配合 /R:0 使用 /NP 不显示进度百分比 减少控制台刷新开销，大批量时显著加快 /NDL 不显示目录列表 减少输出，进一步降低开销 /NFL 不显示文件列表 同上 /NJH 不显示作业头 输出更精简 /NJS 不显示作业摘要 输出更精简 /E 复制所有子目录（含空目录） 常用 /S 复制子目录（不含空目录） /MIR 镜像同步（含删除目标多余文件） 等同于 /E /PURGE，慎用 /Z 可重启模式 会显著降速，仅在网络不稳定时使用 /J 使用未缓冲 I/O 复制大文件时更高效（Win10+） /MT 默认即启用，但显式设置更大值常更快 ⚠️ /Z（重启模式）与 /MT（多线程）不能同时使用，/Z 会让复制慢数倍。本地磁盘复制务必避免 /Z。\n二、推荐命令示例 场景 1：大批量小文件本地复制（最常用）\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /E /MT:32 /R:0 /W:0 /NP /NDL /NFL /NJH 场景 2：镜像同步（保持两边一致）\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /MIR /MT:32 /R:0 /W:0 /NP /NDL 场景 3：网络驱动器/不稳定链路\n1 robocopy \u0026#34;\\\\server\\share\u0026#34; \u0026#34;D:\\local\u0026#34; /E /MT:16 /R:2 /W:5 /Z /NP 网络/不稳定时保留少量重试，用 /Z 支持断点续传（牺牲速度换可靠性）。 场景 4：只复制修改过的文件（增量备份）\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /E /M /MT:32 /R:0 /W:0 /NP /M 只复制带存档属性的文件（即修改过的文件），复制后清除存档属性。 场景 5：复制大文件（电影、数据库、ISO）\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /J /MT:4 /R:0 /W:0 /NP /J 启用未缓冲 I/O，绕过缓存，对大文件更高效；线程数可适当调低。 三、进阶技巧 1. 线程数怎么选？\n小文件多：/MT:32 到 /MT:64（瓶颈在元数据和小 I/O） 大文件多：/MT:4 到 /MT:8（瓶颈在带宽，线程多了反而争抢） 机械硬盘：建议 /MT:8 左右，过多线程会导致磁头频繁寻道，反而变慢 SSD / NVMe：可大胆用 /MT:64 2. 限制带宽（避免占满网络）\n1 robocopy ... /IPG:5 /IPG:N 在每个包之间等待 N 毫秒，缓解网络压力。\n3. 按时间过滤\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /E /MAXAGE:7 /MT:32 /MAXAGE:7 只复制 7 天内修改的文件；还有 /MINAGE、/MAXLAD、/MINLAD 等。\n4. 保留日志便于排查\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /E /MT:32 /R:0 /W:0 /NP /LOG:copy.log 用 /LOG（覆盖）或 /LOG+（追加）输出到文件，比控制台打印快得多。\n5. 先做\u0026quot;干跑\u0026quot;预览\n1 robocopy \u0026#34;D:\\source\u0026#34; \u0026#34;E:\\backup\u0026#34; /E /L /MT:32 /L 只列出将要复制的文件，不实际复制，方便检查范围。\n6. 恢复中断的任务 Robocopy 天然支持断点续传特性：直接重新运行同一命令，它会自动跳过已完整复制的文件（按时间戳和大小判断），无需额外参数。\n四、性能对比参考 复制约 10 万个混合文件时，典型效果：\n普通拖拽复制：受资源管理器单线程限制，较慢 robocopy /E /MT:1（单线程）：基线 robocopy /E /MT:32 /R:0 /W:0 /NP /NDL：通常快 3-10 倍（尤其小文件场景） 总结口诀 多线程 /MT、零重试 /R:0 /W:0、少输出 /NP /NDL /NFL、避开 /Z、大文件加 /J、日志用 /LOG。\n按\u0026quot;文件大小特征 + 存储介质\u0026quot;调整线程数，基本就能拿到 robocopy 的最佳性能。\n","date":"2026-08-25T08:39:00+08:00","permalink":"/p/robocopy%E6%89%B9%E9%87%8F%E6%96%87%E4%BB%B6%E5%A4%8D%E5%88%B6%E4%BC%98%E5%8C%96%E6%8A%80%E5%B7%A7/","title":"robocopy批量文件复制优化技巧"},{"content":"DataBuff 介绍与使用指南 参考来源：DataBuff 官方文档（v0.1.7） https://databuff.ai/docs/zh 仓库：https://github.com/databufflabs/databuff · 许可证：AGPL-3.0\n一、DataBuff 是什么 DataBuff 是一款国产开源、AI 原生的 OpenTelemetry APM（应用性能监控）后端。\n一句话定位：先接入标准遥测数据，再让 AI 读懂你的系统。\n它把两件事做在一起：\nOpenTelemetry APM AI 原生 价值 看清 Trace、指标、拓扑、告警 用对话完成查询、巡检、诊断 OpenTelemetry APM 三大特点 功能完善：基于 OpenTelemetry 标准接入，覆盖应用性能监控全链路 —— 故障排查（服务红绿灯）、链路追踪、服务指标（QPS / 延迟 / 错误率 / JVM）、服务拓扑（自动绘制调用关系）。 告警基础能力：灵活的阈值与突变检测规则、定时评估核心服务指标、记录告警事件。 架构极简：仅 3 个核心组件（接入 + 存储 + 平台），Docker 一条命令即可跑起来，无复杂中间件堆砌，运维成本极低。 AI 三大亮点 AI 原生，不是外挂聊天框：将 LLM 能力与 OpenTelemetry APM 数据原生融合，AI 直接查询 Trace、指标、拓扑、告警，而不是脱离上下文猜答案。 功能丰富：智能问数（自然语言查指标 / Trace / 拓扑 / 告警）、服务巡检（自动发现异常）、故障分析（综合多源数据给出诊断）、MCP 开放（外部 Agent 可调用平台能力）。 AI 架构先进 · 多智能体协同：AI 大脑统一理解意图并分派最合适的专家，数字专家各司其职（问数、巡检、分析），复杂问题多专家并行协作。 适用场景 希望快速落地 APM，又不想维护重型平台 想让研发 / 运维用对话代替查图表 需要开源可私有化的 AI 运维能力 正在评估 AI 原生 OpenTelemetry APM 的技术选型 二、架构介绍 2.1 最小架构：三大核心组件 DataBuff 采用极简的三组件架构，把遥测数据接入、存储、查询与可视化串在一条流水线里，没有多余的中间层：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 OpenTelemetry / SkyWalking Agent │ (OTLP gRPC 4317 / HTTP 4318) ▼ ┌──────────┐ │ Ingest │ 接收数据：OTLP 接入、Trace 二次处理、指标聚合、Stream Load 写 Doris └────┬─────┘ │ (Stream Load / JDBC) ▼ ┌──────────┐ │ Doris │ 统一查询存储：列存与时序查询，Trace/Log/Metric 表按天动态分区（默认保留约 30 天） └────┬─────┘ │ ▼ ┌──────────┐ │ Web │ APM 平台与 AI 专家：REST API、前端、告警引擎、AI 平台与 MCP └──────────┘ Docker 默认四容器：Doris FE/BE、ingest、web。\n2.2 组件职责 组件 职责 Doris 列存与时序查询；Trace / Log / Metric 表按天动态分区（默认保留约 30 天） Web REST API、前端、告警引擎、AI 平台与 MCP 2.3 三种信号如何流转与关联 信号 典型来源 Ingest 处理 Doris 存储（示例） Web 能力 Metrics OTel metrics、JVM/HTTP 等 分钟级聚合 metric_service* 等 服务指标、仪表盘 Logs OTel logs exporter 解析 OTLP log records log_dc_record 日志检索、Trace 关联 三信号关联方式：\nTrace ↔ Log：日志记录可携带 trace_id / span_id（OTel 语义约定）；在 Trace 详情中可关联查看同 trace 的日志行，日志也可反向落到具体 Span。 Trace ↔ Metric：Ingest 从 Span 派生服务级、接口级分钟指标，与 Trace 共享 service / instance 维度。 统一查询面：Web 按服务、时间范围聚合三类数据；AI 诊断以 Trace 与指标为上下文，逐步纳入日志。 2.4 AI 平台架构（多智能体协同） DataBuff 的 AI 不是「外挂聊天框」，而是按 AI 原生设计、直接长在 APM 数据之上。\n核心原则：\n原则 说明 专家分工 不同场景由不同专家处理，比单一模型更准 大脑编排 用户只面对一个入口，复杂协作在后台完成 开放扩展 Skill 定义专家行为，Tool 扩展能力边界 三层能力体系：\n层 作用 举例 Tool 专家可调用的原子能力 查服务列表、查 Trace、画趋势图 Skill 约束专家的行为规则 问数口径、巡检流程、路由策略 新增能力 = 组合 Tool + 编写 Skill + 注册 Expert，不用改核心代码。\n与 APM 的原生融合： 问「错误率」查的是真实 Doris 指标而非幻觉；问「慢 Trace」拉的是真实链路数据；问「服务关系」画的是真实拓扑。这是「AI 原生 OpenTelemetry APM」与「APM + 聊天框」的本质区别。\n开放生态： 支持多模型（OpenAI 兼容、Anthropic 等即配即用）；支持 MCP 协议（外部 Agent 如 Cursor / Claude 可通过标准 /mcp 端点调用平台 APM 工具，平台也可作 MCP 客户端接入外部服务）；Skill 可定制，内置 Skill 可覆盖。\n2.5 数据流地址（默认） 用途 Docker Kubernetes 默认账号 admin / Databuff@123 admin / Databuff@123 OTLP gRPC \u0026lt;本机IP\u0026gt;:4317 \u0026lt;节点IP\u0026gt;:30417 OTLP HTTP http://\u0026lt;本机IP\u0026gt;:4318 http://\u0026lt;节点IP\u0026gt;:30418 K8s 集群内 Agent 上报：http://ai-apm-ingest:4318（gRPC ai-apm-ingest:4317）。\n三、核心优势与亮点 本章是面向技术选型的浓缩版，便于快速决策。架构基础见第二章，与具体竞品的逐项实测对照见第四章。\n3.1 六大核心优势 # 优势 说明 2 7 大 AI 能力开箱即用 看得见（自然语言问系统）→ 军团协同（多 Agent 协同）→ 会巡检（服务巡检 + 报告）→ 会诊断（瓶颈 / 根因取证）→ 会修复（运维专家处置）→ 会预测（容量 / 趋势）→ 会答疑（产品答疑专家）。SkyWalking / Pinpoint / Jaeger 均无等价能力。 3 原生 OpenTelemetry / OTLP，多语言全信号 原生支持 OTLP Trace + Metrics + Logs，一套后端覆盖 Java / Go / Python / Node 等多语言；并兼容 SkyWalking gRPC，可接管现有 SW Agent。 4 APM 纵深：服务/实例/接口三级 + 服务流 + 中间件专页 从「看见连谁」到「谁拖慢、再点进 Trace」：服务级 / 实例级 / 接口级调用分析均可落到 Trace；服务流按入口展开下游响应贡献度；DB / 缓存 / MQ / 外部服务独立专页。 5 极简部署，3 组件一条命令 仅 Ingest + Doris + Web，Docker 一条命令即可跑起来，无复杂中间件堆砌；推荐 8C16G 即可起步。 6 开源可私有化，数据在自己手里 AGPL-3.0 开源，支持私有化部署，遥测数据不出企业网络。 3.2 开放生态亮点 多模型支持：OpenAI 兼容、Anthropic 等即配即用，不绑定单一 LLM 厂商。 MCP 协议双向：DataBuff 既是 MCP 服务端（外部 Agent 如 Cursor / Claude 可通过 /mcp 调用平台 APM 工具），也可作 MCP 客户端接入外部服务。 Skill 可定制：新增能力 = 组合 Tool + 编写 Skill + 注册 Expert，不用改核心代码；内置 Skill 可覆盖。 多智能体协同：AI 大脑统一理解意图并分派专家，数字专家各司其职（问数、巡检、分析），复杂问题多专家并行协作。 3.3 适合谁 / 不适合谁 适合 DataBuff 暂不适合（或需并跑） 技术栈 多语言微服务、已用或愿意用 OpenTelemetry 纯 Java 且强依赖方法级 Call Tree / 字节码 Profiling 诉求 AI + APM 纵深 + 告警一体化、快速落地 强依赖可定制仪表盘 / eBPF Profiling（DataBuff 暂不支持） 迁移意愿 接受改上报地址并跑验证（SW / Jaeger）或换 OTel Agent（Pinpoint） 不愿换任何 Agent 一句话：DataBuff = OpenTelemetry 标准接入 + AI 原生分析 + 极简私有化部署。先用现有 Agent 改上报地址并跑验证，是最稳妥的评估路径。\n3.4 AI 平台的启用前提与能力边界（不配 API Key 能用吗） 这是选型时最常被问到的问题，单独说明。\n关键结论：DataBuff 本身开源免费、不卖 API Key。 这里的「API Key」指你自己从大模型厂商（OpenAI 兼容接口、Anthropic 等）申请的密钥，用来驱动 AI 平台的 LLM 推理。模型推理由外部 LLM 完成，因此必须有可用的模型 API Key 才能启用 AI 能力。\n但 APM 基础功能与 AI 平台是两条相互独立的链路：官方安装后验证把「配置模型」明确标注为可选步骤（第 3 步带「（可选）」），前两步看服务列表、链路、拓扑无需任何模型配置即可完成。\n不配 API Key 配了 API Key AI 平台：问数 / 巡检 / 诊断 / 修复 / 预测 / 答疑 / AI 大脑协同 ❌ 不可用 ✅ 可用 开放扩展：MCP / Skill / 自定义数字专家 ❌ 不可用（依赖 AI 大脑路由） ✅ 可用 不配 API Key 仍可用的功能（OpenTelemetry APM 基础能力）：\n✅ 链路追踪（Trace 列表 / 详情 / 瀑布图） ✅ 服务列表 / 黄金指标（QPS、延迟、错误率、JVM） ✅ 全局拓扑 / 服务流 / 服务-实例-接口三级调用分析 ✅ 中间件专页（DB / 缓存 / MQ / 外部服务） ✅ 日志分析 + Trace↔Log 关联 ✅ 告警中心（阈值规则、突变检测、告警事件） ✅ 数据采集、存储、查询 必须配 API Key 才能用的功能（AI 平台）：\nAI 平台「快速上手」第一步即「配置管理 → 模型配置 → 填入 API Key」，没有这一步无法进入对话：\n❌ 智能问数（自然语言查指标 / Trace / 拓扑 / 告警） ❌ 会巡检（主动发现异常 + 报告） ❌ 会诊断 / 会修复 / 会预测 ❌ 产品答疑专家 ❌ AI 大脑多智能体协同 ❌ MCP / Skill / 自定义数字专家 推荐路径：先不配 Key，把 DataBuff 当标准 OpenTelemetry APM 跑通验证（采集、存储、可视化、告警全部跑得起来，零成本）；确认 APM 数据符合预期后，再填入你已有的任意 OpenAI 兼容模型 Key 解锁 AI 能力。两步相互独立、互不阻塞。\n四、与同类产品对比 以下均为 同机实测对比（同一台 192.168.50.140，同一份 Demo 数据双跑），覆盖三款主流开源 APM：SkyWalking 10.4.0、Pinpoint 3.1.0、Jaeger v1.76.0，对应 DataBuff v0.1.4。标记：✅ 本环境可验证 · △ 有入口但深度有限 · ❌ 无等价能力。\nDataBuff 的总体优势已在第三章概述，本章聚焦与三款产品的逐项差异。\n4.1 AI 能力（差距最大的一组） SkyWalking 无等价 AI 平台；DataBuff 把 7 大能力组织成可配置首页入口，APM 数据直接作 AI 上下文，并支持外接 MCP / Skill 与自定义数字专家。\nAI 能力 SkyWalking 10.4.0 DataBuff ② 军团协同 · 多 Agent 协同 ❌ ✅ 多专家并行取证、串行保上下文；任务可编排复用 ③ 会巡检 · 服务巡检 + 报告 ❌ ✅ 一句话巡检，输出带证据与处置建议的报告 ④ 会诊断 · 瓶颈 / 根因取证 ❌ ✅ 结合 Trace / 指标 / 拓扑拼诊断证据（非黑盒一句「根因」） ⑤ 会修复 · 运维专家处置 ❌ ✅ 策略允许 + 人工授权下执行修复；危险命令 denylist ⑥ 会预测 · 容量 / 趋势 ❌ ✅ 容量与趋势分析，从事后排障拉到事前预判 ⑦ 会答疑 · 答疑专家 ❌ ✅ 检索产品文档与代码，回答部署 / 接入 / 配置问题 外部拓展 · MCP / Skill / 自定义专家 ❌ ✅ 外接 MCP、Skill，并可自定义数字专家扩展排障能力 4.2 APM 应用性能 基础面（拓扑 / 服务列表 / Trace / 日志 / Span↔日志）两侧都有；DataBuff 领先在 服务级 · 实例级 · 接口级调用分析（含关联 Trace）、实例 / 接口级拓扑、服务流、APM 专页与调用分析 / Trace 联动及错误分析纵深，以及 Log→Trace 落到具体 Span。Profiling 与可定制仪表盘深度是 SkyWalking 明显更强、DataBuff 暂未覆盖的两块。\nAPM 能力 SkyWalking DataBuff 服务列表 / 黄金指标 ✅ ✅ 服务级调用分析（上下游指标 + 关联 Trace） ❌ ✅ 可直接落到 Trace 实例级拓扑 / 实例级调用分析 ❌ ✅ 接口级拓扑 / 接口级调用分析 ❌ ✅ 服务流（服务级 / 接口级 Trace 链路分析） ❌ ✅ 按入口展开下游响应贡献度 中间件 / 外部调用专页（DB / 缓存 / MQ） ✅ Dashboard 大盘 ✅ 独立 APM 专页 + 联动调用分析 / Trace 错误分析（统计 + 接口级） ❌ ✅ Trace 列表 / 详情 / Span 关联日志 ✅ ✅ Log → Trace ✅ ✅ 并可落到具体 Span Profiling（Tracing / AsyncProfiler / eBPF） ✅ 三类均支持 ❌ 暂不支持 可定制仪表盘 ✅ ❌ 暂不支持 4.3 告警 差异主要在两头：怎么配（后端文件 vs 告警中心）和配完能干什么（查事件 / hooks vs 列表 + 智能告警 + 回服务上下文）。\n告警能力 SkyWalking DataBuff 阈值告警 △ 靠后端 YAML / MQE 表达式维护 ✅ 阈值规则可在平台内管理 智能告警 ❌ ✅ 与 APM 指标联动 告警事件列表 △ ✅（等级 / 服务 / 时间） 告警落到服务 / 中间件 △ 触发后多靠 hooks 通知 ✅ 列表直接挂服务 / 中间件，可回 APM 下钻 4.4 总体对比 对比维度 传统 APM（SkyWalking 等） DataBuff 部署复杂度 组件多、资源重 3 组件，极简部署，一条命令 排障方式 人翻图表 对话式智能分析 接入标准 私有协议为主 原生 OpenTelemetry / OTLP，并可接管现有 SkyWalking / Pinpoint / Jaeger Agent 数据归属 私有化 开源可私有化，数据在自己手里 4.5 对比 Pinpoint（v0.1.4 vs Pinpoint 3.1.0） 同机双跑：DataBuff 走 OTLP :4318，Pinpoint 走官方 quickstart Java Agent（Web :18080 / Demo :18085）。\nAI 能力：Pinpoint 无 AI 平台，7 大能力与 MCP / Skill / 自定义专家全部 ❌，DataBuff 全部 ✅。\n","date":"2026-08-24T10:50:00+08:00","permalink":"/p/databuff-%E4%BB%8B%E7%BB%8D%E4%B8%8E%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/","title":"DataBuff 介绍与使用指南"},{"content":"一、背景 Stack 主题切换上线后（见《博客主题从 MemE 切换到 Stack 实录》），博客外观与基础链路就绪。此后围绕头像、导航、视觉细节、字体、评论与社交做了一系列个性化定制，本文记录这些改动及其背后的取舍与踩坑。整个过程延续「无本地 hugo、直接靠 CI 验证」的工作方式，每个改动都是一次 push → 触发构建 → curl 线上确认的闭环。\n二、改动清单 1. 博客头像 用户提供了一张《猫和老鼠》指挥家横图（1102×689）。Stack 侧边栏头像经 helper/image.html 的 .Resources.Get 解析，而 .site-logo 的 CSS 没有 object-fit（默认 fill 会拉伸），直接用横图会被拉变形。\n处理：用 Pillow 把原图中心裁成正方形 689×689，存 assets/img/avatar.jpeg，config 里 [params.sidebar] avatar = \u0026quot;img/avatar.jpeg\u0026quot;，原图备份为 avatar-source.jpeg。\n1 2 3 4 5 6 from PIL import Image img = Image.open(\u0026#34;avatar-source.jpeg\u0026#34;) w, h = img.size s = min(w, h) left = (w - s) // 2 img.crop((left, 0, left + s, s)).save(\u0026#34;avatar.jpeg\u0026#34;) 关键点：头像必须在 assets/ 下（Stack 用 Hugo Resources 解析，不是 static/），且文件扩展名要与实际文件一致，否则 .Resources.Get 匹配不到。\n2. 分类菜单 404 修复 切换后点侧边栏「技术 / 随笔 / 关于」全跳 404。排查发现菜单 url 配的是 /categories/tech/（categoryMap 的小写英文 key），但 Notion Action 把文章的 Notion Category（中文 select 名「技术/随笔/关于」）原样写进 front matter 的 categories: 字段——categoryMap 只决定 content/zh 下的子目录名，不影响 front matter。Hugo 按 front matter 的 categories 生成分类页，实际路径是中文 /categories/技术/，与菜单对不上。\n1 2 3 # 实测验证 curl /categories/tech/ -\u0026gt; 404 curl /categories/技术/ -\u0026gt; 200 修复：把菜单三个 url 改成 Hugo 实际生成的中文路径 /categories/技术/、/categories/随笔/、/categories/关于/。随笔暂无文章，/categories/随笔/ 仍 404，等有文章后自动出现，保留菜单项占位。\n教训：Notion→Hugo 链路里，分类页 URL 由 front matter 的 categories 决定，不由文件目录决定；改菜单前先 curl 确认 Hugo 实际生成的路径。\n3. 页脚跳动的心 参考 guanqr.com，在版权行末尾加一颗 Font Awesome 实心心形，用 fa-heartbeat 双跳动画（1.3s 循环）。通过覆盖主题 partial 实现，不动 submodule：\nlayouts/_partials/footer/footer.html：版权行追加 （FA 心形路径） layouts/_partials/footer/custom.html：注入心跳 @keyframes CSS（主题留空的扩展点） 1 2 3 4 5 6 7 8 @keyframes heartbeat { 0% { transform: scale(1); } 14% { transform: scale(1.3); } 28% { transform: scale(1); } 42% { transform: scale(1.3); } 70% { transform: scale(1); } 100% { transform: scale(1); } } 细节：心形红色 #ff4d4f，1em 尺寸随字号缩放，vertical-align 对齐基线；加 @media (prefers-reduced-motion: reduce) 守卫，系统开了「减少动态效果」时停止动画（无障碍）。\n4. 字体：中文霞鹜文楷 + 代码 JetBrains Mono 目标是中文用霞鹜文楷、代码用 JetBrains Mono。采用混合方案：JetBrains Mono 走 Google Fonts CDN，霞鹜文楷只设 font-family 名走系统回退——用户本地装了才显示，省去几 MB webfont 的网络开销。\nStack 主题的字体由三个 CSS 变量控制（在 variables.scss）：--base-font-family（界面）、--article-font-family（正文，继承 base）、--code-font-family（代码）。覆盖方式：\nlayouts/_partials/head/custom-font.html：把加载的 Google Fonts 从 Lato 换成 JetBrains Mono assets/scss/custom.scss：重定义三个 CSS 变量 1 2 3 4 5 6 7 :root { --base-font-family: \u0026#34;LXGW WenKai Screen\u0026#34;, \u0026#34;LXGW WenKai\u0026#34;, \u0026#34;霞鹜文楷\u0026#34;, -apple-system, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Microsoft YaHei\u0026#34;, sans-serif; --article-font-family: var(--base-font-family); --code-font-family: \u0026#34;JetBrains Mono\u0026#34;, \u0026#34;LXGW WenKai Mono\u0026#34;, Menlo, Monaco, Consolas, monospace; } 生效原理：style.scss 里 @import \u0026quot;custom.scss\u0026quot; 在 variables.scss 之后，我的 :root 重定义排在主题默认值之后，CSS 变量后者覆盖前者。霞鹜文楷回退链：LXGW WenKai Screen → LXGW WenKai → 霞鹜文楷 → 系统无衬线，未装则回退 PingFang SC / 微软雅黑。\n踩坑：custom.scss 是静态资源，不经过 Hugo 模板引擎，只能用 CSS 注释，不能写 {{}}（否则 SCSS 编译报错）。\n5. 评论系统 Giscus + 社交链接 评论选 Giscus（基于 GitHub Discussions，与 GitHub Pages 技术栈契合，免费无广告）。社交在原有 GitHub + RSS 基础上加 CSDN 和 Email。\nGiscus 需要 repo-id 和 category-id。这两个不必走 giscus.app 配置页，直接用 GitHub API 取：\n1 2 3 4 5 6 7 # 启用 Discussions（REST PATCH） curl -X PATCH -H \u0026#34;Authorization: token $TOKEN\u0026#34; \\ https://api.github.com/repos/vamViolet/notion-hugo-meme \\ -d \u0026#39;{\u0026#34;has_discussions\u0026#34;: true}\u0026#39; # 取 repo-id 和 category-id（GraphQL） # query: repository(owner,name){ id, discussionCategories(first:20){ nodes{ id name slug } } } 拿到 repo-id R_kgDOTllGGw、Announcements 分类 id DIC_kwDOTllGG84DDR7I，写入 config [params.comments.giscus]。mapping=title 按标题关联 discussion，文章 URL 变了也不丢评论；lightTheme/darkTheme 跟随博客明暗主题。\n关键点：Stack 单篇文章的 comments front matter 字段会覆盖全局 [params.comments.enabled]。所以光开全局不够，还要把 archetype 和现有文章的 comments: false 全改成 true（9 篇批量 sed）。\n社交：Stack 侧边栏图标经 helper/icon.html 的 resources.GetMatch \u0026quot;icons/.svg\u0026quot; 解析，主题内置图标没有 email/csdn，于是自己画了 assets/icons/email.svg（Tabler mail 风格）和 assets/icons/csdn.svg，在 [menu.social] 引用。\n三、踩坑记录 坑 1：Docker Action 生成文件的权限 某次同步新文章，fix-tags 步骤报 PermissionError: [Errno 13] Permission denied。根因：rxrw/notion-blog 是 Docker action，在容器内以 root 生成 content/zh、static/images 文件，归 root 所有；后续 fix-tags / fix-dates 以 runner 用户运行，写或删这些文件就失败。这个潜在 bug 只有在需要改新文件时才暴露——只读的步骤会蒙混过去，所以前面几次构建一直没发现。\n修复：在 notion-blog 步骤之后、cleanup 之前加 chmod -R a+rwX content/zh static/images，一次性放开权限。\n坑 2：Notion created_time 不可控，迁移文章日期全变成今天 迁移 9 篇 Gridea 老文章时，Notion Action 用页面 created_time 作为文章 date。但 created_time 是只读系统字段，API 创建时自动设为 now，无法覆盖。结果 2021/2023 的老文章同步后全变成今天的日期，堆在归档页顶部当「最新」。\n解法：加自愈 workflow 步骤——scripts/migrated-dates.json（title→原日期映射）+ scripts/fix-migrated-dates.py，每次 sync 在 fix-tags 之后、commit 之前重写匹配文章的 date/lastmod 回原日期。幂等，每次同步都跑，文件已在正确日期则不动。\n坑 3：Notion 链接必须是绝对 URL 创建 Notion 页面时，两篇文章 block append 报 Invalid URL for link。排查是源 HTML 里有相对锚点（#fn1 脚注、%E7%9B%AE%E5%BD%95 编码的「目录」TOC 链接）。Notion 的 link 字段只接受绝对 http(s) URL。修复：在转换器 text_seg 里加 valid_url 校验，非法链接保留文字、丢弃 link，避免阻断整批 block 写入。\n坑 4：mybatisplus 图片 404 2 张 mybatisplus 图片原图在 vamViolet.github.io/post-images/，Action 的 getImage 把文件存到 static/images/post-images/ 子目录时 os.Create 失败（子目录不存在），回退写出裸路径 /post-images/X.png 导致 404。解法：Hugo 把 static/ 全部映射到站点根，直接把图放到 static/post-images/X.png 即可让 /post-images/X.png 解析。文章 Status=Published 不会再被 Action 覆盖，此补丁持久。\n四、验证方法 全程无本地 hugo，靠 CI 验证：改完 push → workflow_dispatch 触发 → 轮询 run 状态 → 成功后 curl 线上页面抓 HTML/CSS 确认元素和变量生效。\n1 2 3 4 5 6 7 8 # 触发构建（用 git credential 里的 token，无需 gh CLI） TOKEN=$(printf \u0026#34;protocol=https\\nhost=github.com\\n\\n\u0026#34; | git credential fill | sed -n \u0026#34;s/^password=//p\u0026#34;) curl -X POST -H \u0026#34;Authorization: token $TOKEN\u0026#34; \\ https://api.github.com/repos/vamViolet/notion-hugo-meme/actions/workflows/notion-blog.yml/dispatches \\ -d \u0026#39;{\u0026#34;ref\u0026#34;:\u0026#34;master\u0026#34;}\u0026#39; # 验证字体变量在编译后 CSS 里（minify 后有两个 :root 定义，后者覆盖前者） curl https://notion.dongxiaoqi.top/scss/style.min.\u0026lt;hash\u0026gt;.css | grep -o \u0026#39;JetBrains Mono\u0026#39; 五、最终效果 线上 https://notion.dongxiaoqi.top/ 已确认：\n侧边栏头像（Tom \u0026amp; Jerry 指挥家，正方形裁剪不拉伸） 技术 / 关于 菜单可正常打开（中文分类页），随笔待文章 页脚版权行末尾跳动的心（红色，1.3s 双跳） 中文霞鹜文楷（系统装了才显示）、代码 JetBrains Mono 文章页 Giscus 评论框（参数齐全，待装 App）、侧边栏 CSDN / Email / GitHub / RSS 四图标 六、遗留事项 Giscus App 待手动安装：https://github.com/apps/giscus → Install → 授权 notion-hugo-meme 仓库，否则评论框报错 Notion token 在对话历史明文出现过，建议轮换并更新 GitHub NOTION_TOKEN secret 随笔分类待补文章，/categories/随笔/ 目前 404 ","date":"2026-08-13T11:12:00+08:00","permalink":"/p/stack-%E4%B8%BB%E9%A2%98%E4%B8%AA%E6%80%A7%E5%8C%96%E5%AE%9A%E5%88%B6%E5%AE%9E%E5%BD%95/","title":"Stack 主题个性化定制实录"},{"content":" 一次完整的话题切换实操记录：从 MemE 换到 hugo-theme-stack，顺带把被钉死的 Hugo 老版本解锁。包含三个真实踩坑和 CI 迭代验证过程。\n一、背景 博客原本用 MemE 主题（git submodule，钉在 2022 年的 v5.0.0）。MemE v5.0.0 用了旧版 resources.ToCSS API，与新版 Hugo 不兼容，导致 CI 工作流被迫把 Hugo 钉在 0.112.7——这是个 2023 年的老版本，越来越难维护，新特性也用不了。\n目标：换成更现代的主题，并顺带解除旧 Hugo 版本锁。\n二、选型 候选了三个主流主题，最终选 hugo-theme-stack：\n卡片式文章列表、左侧边栏、原生暗色模式切换 维护活跃，文档完善 要求 Hugo ≥ 0.157.0 extended，正好顺带升级 三、改动清单 1. 替换主题 submodule 移除 MemE，新增 Stack 并锁定到稳定 tag：\n1 2 3 4 5 git submodule deinit -f themes/meme git rm -f themes/meme rm -rf .git/modules/themes/meme git submodule add -b master https://github.com/CaiJimmy/hugo-theme-stack.git themes/stack git -C themes/stack checkout v4.0.3 2. 重写 config.toml 从 1432 行的 MemE 配置精简到约 210 行。关键调整：\ntheme = \u0026quot;stack\u0026quot; mainSections = [\u0026quot;zh\u0026quot;]：核心，让首页读 content/zh/（Notion Action 同步目录，不改 Action 也不改清理脚本） [permalinks] zh = \u0026quot;/p/:slug/\u0026quot;：用 Stack 默认 URL 风格，文章 URL 从 /zh/xxx/ 变 /p/xxx/ [menu] 改成 Stack 的 main + social 格式 删除 MemE 专属的 [params]、[outputFormats]、[outputs] 等几百行 3. 重写 archetypes/notion.md Notion Action 同步时套这个模板。改成 Stack 的 front matter 字段，两个关键点：\ndraft: false：Stack 默认 archetype 是 draft: true，不显式改 false 的话同步出的文章会被当草稿隐藏 category → categories（复数）：Stack 模板用 .Params.categories 判断分类，单数 key 不会显示分类徽章 4. 升级 Hugo 工作流里 0.112.7 → 0.163.3，extended: true 保留。\n四、踩坑记录 整个过程走了三轮 CI 迭代，每个坑都是一次失败构建。\n坑 1：计划里的 Hugo 0.157.2 根本不存在 第一轮按计划填 hugo-version: '0.157.2'，peaceiris/actions-hugo 直接报错：\n1 Unable to find a compatible Hugo release asset for this runner. 查 Hugo releases 才发现：0.157 线只有 0.157.0，之后直接跳到 0.160+，根本不存在 0.157.1/0.157.2。改成实际存在的 0.163.3（成熟 .3 补丁版本，远高于 Stack 的 min 0.157.0）解决。\n教训：版本号要查 releases 确认存在，别想当然填补丁号。\n坑 2：菜单图标不在 Stack 内置集 第二轮 Hugo 装上了，但构建报：\n1 2 ERROR Error: icon \u0026#39;code.svg\u0026#39; is not found under \u0026#39;assets/icons\u0026#39; folder ERROR Error: icon \u0026#39;notes.svg\u0026#39; is not found under \u0026#39;assets/icons\u0026#39; folder 菜单里 技术 用了 icon = \u0026quot;code\u0026quot;、随笔 用了 icon = \u0026quot;notes\u0026quot;，但 Stack 只内置 24 个 SVG（themes/stack/assets/icons/），不含 code/notes。改成内置图标：技术→categories、随笔→archives、关于→user。\n教训：主题里引用的资源（图标、图片）要先确认主题是否自带，没有就得自己往 assets/ 放。\n坑 3：languageCode 弃用警告 第三轮构建成功，但有 WARN：\n1 WARN deprecated: project config key languageCode was deprecated in Hugo v0.158.0 修这个又踩了一层：顶层 languageCode 在 0.158 弃用，移到 [languages.zh].languageCode 后，0.163 又把这个键改名为 locale（连同 languageName → label）。最终用最新键名：\n1 2 3 4 5 [languages] [languages.zh] locale = \u0026#34;zh-CN\u0026#34; label = \u0026#34;中文\u0026#34; weight = 1 额外风险：显式加 [languages.zh] 后，Hugo 可能把 content/zh/ 当成 zh 语言根目录（而非 section），导致 mainSections=[\u0026quot;zh\u0026quot;] 失配、首页变空。本地用下载的 hugo 二进制构建验证过：仍是 section，首页 2 篇文章、/p/ URL 都没变，才敢推 CI。\n五、验证方法 这次没用本地 hugo server 预览，直接靠 CI 验证：\ngit push 后用 GitHub API 手动触发 workflow_dispatch 轮询 run 状态到 completed 失败就下载日志 zip，定位报错步骤，修复重推 成功后抓线上首页 HTML，核对 Stack 标记、文章链接、CSS/JS 资源是否 200 三轮迭代，每轮约 2–3 分钟，比反复起本地服务更快也更接近真实部署环境。\n六、最终效果 线上 https://notion.dongxiaoqi.top/ 已确认：\nStack 外观：左侧边栏、卡片式文章列表、暗色模式切换 首页正常显示 2 篇文章，URL 是 /p/xxx/ 文章页正文、标题、代码块、标签、阅读时长都正常 /tags/ 三个标签（Hugo / Notion / GitHub-Pages）正常 CSS（55KB）/ JS（8KB）资源都 200，无样式崩坏 旧 /zh/xxx/ URL 已 404（符合预期，文章少无外链，可接受） 七、语言切换器的实现 主题切换上线后，照着 demo 站（demo.stack.cai.im）加了侧边栏的语言切换下拉框。这一节记录实现机制和一个非标准目录结构下踩的坑。\n机制：hugo.IsMultilingual Stack 的切换器在 themes/stack/layouts/_partials/sidebar/left.html，是一个 下拉框，遍历 .Site.Home.AllTranslations 生成 option，每个指向对应语言首页 URL、label 用 .Language.Label。但它被包在一个前置条件里：\n1 2 3 {{ if hugo.IsMultilingual }} \u0026lt;li id=\u0026#34;i18n-switch\u0026#34;\u0026gt; ... \u0026lt;select\u0026gt; ... \u0026lt;/li\u0026gt; {{ end }} 也就是说，只有 Hugo 处于多语言模式（[languages] 里定义了 ≥2 种语言）时切换器才渲染。demo 站每种语言都有真实翻译内容，多语言模式天然开启，切换器自然出现。单语博客要显示它，就得显式加第二种语言。\n坑：content/zh 当 section 用，content/en 进不了英语语言 本站的内容目录结构是非标准的：文章在 content/zh/，靠 mainSections=[\u0026quot;zh\u0026quot;] + permalinks zh=\u0026quot;/p/:slug/\u0026quot; 让首页读到它、文章落在 /p/。这是为了不动 Notion Action 硬编码的 contentFolder。\n加第二种语言时本能地想建 content/en/ 放英语内容——但本地 hugo 实测发现行不通：默认语言 zh 且 defaultContentLanguageInSubdir=false 时，Hugo 从 content/ 根扫描，把 content/ 下所有子目录（含 content/en/）都当成 zh 的 section。结果 content/en/ 永远成不了英语语言根，英语首页 RegularPages=0，空列表。\n换成 defaultContentLanguageInSubdir=true 也没用，content/en/ 依旧被默认语言吞成 section（变成 /zh/en/posts/...）。这个坑用调试 partial 打印 .Site.RegularPages 的 type/url 才定位到。\n解法：by-filename 后缀 .en.md 改用 Hugo 的 by-filename 机制：文件名带 ..md 后缀的会归对应语言，且继承所在目录的 section。英文欢迎页放在 content/zh/welcome.en.md——.en 后缀让它归英语语言，content/zh/ 目录让它共享 section \u0026quot;zh\u0026quot;，于是 mainSections 和 permalinks 完全不用改。\n效果：中文文章仍在 /p/:slug/（URL 零变化），英文欢迎页在 /en/p/welcome/，切换器选项 中文(/) ↔ English(/en/)。\n配套改动 config.toml 加 [languages.en]（locale=\u0026quot;en-US\u0026quot;, label=\u0026quot;English\u0026quot;, weight=2），开启 IsMultilingual，切换器自动渲染。 新建 content/zh/welcome.en.md：英文欢迎页，说明本站主语言为中文、英文版建设中，附返回中文首页的链接。 工作流清理脚本加 TRANSLATION_SUFFIXES=(\u0026quot;.en.md\u0026quot;,) 跳过翻译文件——否则每次 CI 会 git rm 掉欢迎页（Notion Action 不产生 .en.md，它不在 expected 集合里）。已实测：found 2 .md file(s); 0 to delete，欢迎页安全。 后续加英文内容 想加英文文章，按同样规则放 content/zh/.en.md 即可，自动出现在 /en/ 列表、URL 为 /en/p//。若英文文章多了，建议重新评估是否把 Notion 同步改成真正的多语言结构（那是另一个项目）。\n八、遗留事项 分类徽章不显示：现有两篇文章 front matter 是 category: 首页（单数 key），Stack 读 categories（复数）。新 archetype 已改复数，下次 Notion 重新同步时自动修好。 Notion token 安全：集成 token 曾在对话中明文出现，建议去 Notion 重新生成并更新 GitHub 仓库的 NOTION_TOKEN secret。 九、关键命令速查 触发 CI 构建（用仓库存储的 GitHub 凭据，无需 gh CLI）：\n1 2 3 4 5 6 # 取 git credential 里的 token TOKEN=$(printf \u0026#34;protocol=https\\nhost=github.com\\n\\n\u0026#34; | git credential fill | grep \u0026#39;^password=\u0026#39; | sed \u0026#39;s/^password=//\u0026#39;) # 手动触发 workflow curl -X POST -H \u0026#34;Authorization: Bearer $TOKEN\u0026#34; \\ -d \u0026#39;{\u0026#34;ref\u0026#34;:\u0026#34;master\u0026#34;}\u0026#39; \\ https://api.github.com/repos/vamViolet/notion-hugo-meme/actions/workflows/notion-blog.yml/dispatches 下载某次 run 的日志（API 返回 302 到 S3，要跟跳转）：\n1 2 3 4 5 REDIR=$(curl -D - -o /dev/null -H \u0026#34;Authorization: Bearer $TOKEN\u0026#34; \\ https://api.github.com/repos/vamViolet/notion-hugo-meme/actions/runs/\u0026lt;RUN_ID\u0026gt;/logs \\ | grep -i \u0026#39;^location:\u0026#39; | sed \u0026#39;s/^[Ll]ocation: //\u0026#39;) curl -o logs.zip \u0026#34;$REDIR\u0026#34; unzip logs.zip ","date":"2026-08-12T12:45:00+08:00","permalink":"/p/%E5%8D%9A%E5%AE%A2%E4%B8%BB%E9%A2%98%E4%BB%8E-meme-%E5%88%87%E6%8D%A2%E5%88%B0-stack-%E5%AE%9E%E5%BD%95/","title":"博客主题从 MemE 切换到 Stack 实录"},{"content":"笔记本连WiFi仅微信能用、浏览器/软件无网 完整修复方案 一、快速判断核心原因 微信不走完整网页网络，只走UDP即时通讯通道；浏览器、抖音、QQ、软件需要DNS解析、HTTP/HTTPS网络，故障90%是：DNS异常、代理乱开、IP配置错误、网卡缓存损坏。\n二、按顺序一步步修复（优先简单操作） 步骤1：关闭系统代理（最高发问题） Windows10/11：设置 → 网络和Internet → 代理 全部开关关闭：自动检测设置、使用设置脚本、手动设置代理 关闭后重启浏览器测试 步骤2：重置DNS（最有效） 右键开始菜单 → 打开 命令提示符(管理员) / Windows终端管理员 依次输入下面4行命令，每行输完回车： 1 2 3 4 ipconfig /release ipconfig /flushdns ipconfig /renew netsh winsock reset 执行完成重启电脑，再上网测试 步骤3：手动修改公共DNS（解决解析失败） 右下角WiFi图标右键 → 打开「网络和共享中心」→ 更改适配器设置 右键当前WiFi适配器 → 属性 找到 Internet协议版本4 (TCP/IPv4)，双击打开 选择「使用下面的DNS服务器地址」，填入： 首选DNS：223.5.5.5（阿里公共DNS） 备用DNS：114.114.114.114 确定保存，重新断开WiFi重连 步骤4：检查IP获取方式 同上IPv4设置，确认勾选 自动获取IP地址，不要手动填静态IP。\n三、进阶故障处理（上面无效再操作） 1. 关闭第三方加速器/梯子软件 各类网游加速器、VPN、翻墙工具会篡改系统网络参数，全部彻底退出，重启电脑。\n2. 重装无线网卡驱动 右键开始 → 设备管理器 → 网络适配器 找到带 Wi-Fi/Wireless 字样的无线网卡 右键：卸载设备，勾选「删除此设备的驱动程序软件」 重启电脑，系统会自动重装驱动；若无效去笔记本官网下载对应型号WiFi驱动手动安装。 3. 防火墙/杀毒软件拦截 临时关闭Windows Defender防火墙、360、火绒等安全软件，测试能否打开网页； 如果恢复网络，在杀毒软件里恢复默认网络防护规则。\n四、补充区分：手机连同一WiFi正常 如果手机同WiFi全网正常，问题100%出在笔记本本机网络配置，不用排查路由器，直接执行上面方案即可。\n极简总结操作顺序 关代理 → 管理员CMD重置网络 → 更换公共DNS → 重启电脑 95%的该类故障一次解决。\n","date":"2026-08-11T01:59:00+08:00","permalink":"/p/%E7%AC%94%E8%AE%B0%E6%9C%AC%E8%BF%9E-wifi-%E4%BB%85%E5%BE%AE%E4%BF%A1%E8%83%BD%E7%94%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%8E%E8%BD%AF%E4%BB%B6%E6%97%A0%E7%BD%91%E7%9A%84%E5%AE%8C%E6%95%B4%E4%BF%AE%E5%A4%8D%E6%96%B9%E6%A1%88/","title":"笔记本连 WiFi 仅微信能用、浏览器与软件无网的完整修复方案"},{"content":"notion-hugo-meme 操作手册 本项目把 Notion 数据库作为内容源，通过 GitHub Actions 自动同步为 Markdown，再用 Hugo 构建成静态博客并部署到 GitHub Pages。本文档梳理项目结构，并给出从「写文章」到「上线/下线」的完整操作流程。\n仓库地址：https://github.com/vamViolet/notion-hugo-meme 博客地址：https://notion.dongxiaoqi.top/\n一、整体架构 数据流是单向的、全自动的（每 2 小时触发一次，也可手动触发）：\n1 2 3 4 5 Notion 数据库 ──(rxrw/notion-blog Action)──\u0026gt; content/zh/*.md ──(Hugo build)──\u0026gt; GitHub Pages │ │ └─ 把 Status=Finished 的文章拉成 md └─ https://notion.dongxiaoqi.top/ └─ 同步成功后把 Status 改成 Published └─ 清理步骤：删除已下线文章的 md 三个关键角色：\nNotion 数据库：唯一的内容编辑入口。每篇文章是一条记录，Status 控制是否上线。 GitHub 仓库：存放 Hugo 站点源码、同步出来的 md、以及自动化工作流。 GitHub Pages：托管构建产物，对外提供博客访问。 二、项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 notion-hugo-meme/ ├── .github/workflows/notion-blog.yml # 核心：同步 + 清理 + 构建 + 部署 的工作流 ├── config.toml # Hugo 站点配置（站名、域名、主题、菜单等） ├── notionblog.config.json # notion-blog Action 的配置（数据库 ID、字段映射、过滤规则） ├── archetypes/ │ ├── default.md # `hugo new` 默认模板 │ └── notion.md # Notion 同步时套用的 front matter 模板 ├── content/zh/ # 同步出来的文章 md（按 Category 分子目录） │ └── 建筑服务能力开发.md ├── static/ │ ├── CNAME # 自定义域名（notion.dongxiaoqi.top） │ └── images/ # 文章图片（Action 自动下载到这里） ├── themes/meme/ # MemE 主题（git submodule，指向 rxrw/hugo-theme-meme） ├── resources/ # Hugo 构建缓存 └── README.md 关键配置文件 notionblog.config.json —— 决定 Action 怎么从 Notion 拉文章：\n字段 含义 filterProp / filterValue 只同步 Status 为 Finished 的文章 publishedValue 同步成功后把 Status 改成 Published propertyCategories / categoryMap Category 字段映射：技术→tech、随笔→essay、关于→about，首页→根目录 contentFolder 同步到的目录：content/zh imagesFolder / imagesLink 图片下载到 static/images/，链接前缀 /images config.toml —— Hugo 站点配置要点：\nbaseURL = \u0026quot;https://notion.dongxiaoqi.top/\u0026quot;：自定义域名，构建时不能用 github.io URL 覆盖，否则内链全 404。 title = \u0026quot;dongxiaoqi's Blog\u0026quot;：站点名。 theme = \u0026quot;meme\u0026quot;：使用 MemE 主题（submodule，v5.0.0，2022 年版）。 .github/workflows/notion-blog.yml —— 两个 Job：\nauto-sync-from-notion-to-github：checkout → 跑 notion-blog Action 拉文章 → 清理已下线文章 → 提交推送。 build-and-deploy：拉最新提交 → Hugo 0.112.7 extended 构建 → 部署到 GitHub Pages。 三、Notion 数据库字段 每篇文章（一条记录）需要这些属性：\n属性 类型 作用 Status 状态（status） 控制上线：Finished/Published = 在线，其余 = 下线 Category 单选（select） 分类目录：首页/技术/随笔/关于 Tags 多选（multi_select） 标签 Created by 创建者（created_by） Action 读取作者名（硬编码依赖，缺了会报错） Status 状态机 Status 含义 博客上是否可见 In progress（进行中） 写到一半 否 Finished 写完待发布 → 下次同步会拉取并上线 是（同步后） Published 已上线（同步后 Action 自动置为此状态） 是 Offline（已下线） 主动下线 → 清理步骤会删掉对应 md 否 注意：博客是纯静态站点，没有登录鉴权，所以「私密」等价于「下线」——只要 Status 不是 Finished/Published，文章就不会出现在博客上。\n四、日常操作 4.1 发布一篇新文章 在 Notion 数据库里 New 一条记录，填好 Name（标题）、Category、Tags。 在正文里写内容（支持 Markdown 风格、代码块、表格、图片）。 把 Status 设为 Finished。 等待下次 workflow 触发（最多 2 小时），或去 GitHub 仓库 Actions 页面手动 Run workflow。 同步成功后，文章出现在博客，Status 自动变成 Published。 4.2 修改已发布文章 直接在 Notion 改正文/标题/分类，保持 Status 为 Finished 或 Published，下次同步会更新对应 md。\n想立即触发同步，又不想等 2 小时：GitHub 仓库 → Actions → notion-blog → Run workflow。\n4.3 下线一篇文章 把 Status 从 Finished/Published 改成 Offline（或 Todo/In progress）即可。下次 run 的清理步骤会自动 git rm 掉对应 md，文章从博客消失。\n4.4 改分类 / 改标题 改分类：在 Notion 改 Category。注意：旧分类目录下的旧 md 不会自动删除，需手动清理或等清理步骤处理（清理是按「当前 Notion 里 Finished/Published 的文章」对账的）。 改标题：会生成新文件名的 md，旧文件名的 md 会成为孤儿，下次清理步骤会自动删除。 五、首次部署 / 排错清单 如果同步或部署出问题，按这个顺序排查：\nNotion 数据库是否关联了 Integration：数据库右上角 ... → Connections → 添加你创建的 Integration。没关联会 404。 GitHub Secret 是否正确：仓库 Settings → Secrets and variables → Actions，需有 NOTION_TOKEN（值是 Notion Integration token，ntn_ 开头）。 Notion 字段是否齐全：Name/Status/Category/Created by 必须存在且类型正确，缺了 Action 会报错（但 Action 会吞掉错误、显示成功，要看 Actions 日志才能发现）。 GitHub Pages 源是否设为 Actions：仓库 Settings → Pages → Source 选 GitHub Actions（不是 gh-pages 分支）。 域名/CNAME：static/CNAME 必须是裸域名 notion.dongxiaoqi.top，不能带 http://。 Hugo 版本：工作流固定用 0.112.7 extended。MemE v5.0.0 与新版 Hugo 的 resources.ToCSS 不兼容，不要升到 latest，除非先升主题 submodule。 六、已知限制 Action 只增不删：rxrw/notion-blog 只会新增/更新 md，不会删除。下线靠本仓库自加的清理步骤实现。 正文只取前 100 个 block：Action 不做 block 分页，超长文章（\u0026gt;100 个 Notion block）会被截断。 错误被吞：Action 内部出错只 log.Println 后 exit 0，永远显示「成功」。排查要看 Actions 的完整日志。 属性名硬编码：Action 代码里写死了 Name/Category/Created by 等英文字段名，改不了。 **Tags 渲染缺陷：**rxrw/notion-blog 用 tags : {{.Tags}} 渲染多选标签，Go 切片默认输出 [a b c]（空格分隔无逗号），Hugo 会当成一个名为 “a b c” 的畸形标签。本仓库工作流加了 Fix tags front-matter format 步骤，同步后自动改成逗号分隔 [a, b, c]，博客 /tags/ 页和文章页才能正确显示多个独立标签。 Tag 命名约束：Notion 的 tag 名不要含空格（如用 GitHub-Pages 而非 GitHub Pages）。Action 输出端是空格分隔，含空格的标签名无法无损还原，会被错误拆成多个。中文标签不受影响（如 生产力）。 七、相关链接 仓库：https://github.com/vamViolet/notion-hugo-meme 博客：https://notion.dongxiaoqi.top/ 主题：https://github.com/rxrw/hugo-theme-meme Action：https://github.com/rxrw/notion-blog ","date":"2026-07-29T03:05:00+08:00","permalink":"/p/notion-hugo-meme-%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8C/","title":"notion-hugo-meme 操作手册"},{"content":"skyWalking自动建表-逻辑梳理 使用skyWalking后，发现我们不需要创建表，启动skywalking会自动创建表，遂研究官方源码，感觉oap-server设计的自动建表功能很强大，并进行逻辑梳理，仅供参考。\n源码地址：https://github.com/apache/skywalking.git\n架构图 Agent（代理/探针） ：负责从应用中，无侵入式的收集，并通过HTTP或者gRPC方式发送数据到SkyWalking OAP 服务器； SkyWalking OAP ：负责接收Agent发送的Tracing数据信息，然后进行分析(Analysis Core)，存储到外部存储器(Storage)，最终提供查询(Query)功能； Storage：Tracing数据存储，目前支持ES、MySQL、Sharding Sphere、TiDB、H2等多种存储器，SkyWalking开发团队自己的生产环境采用ES为主； SkyWalking UI：负责提供控制台，查看链路等等； 建表逻辑 1、启动时执行脚本\n2、脚本内容指向：org.apache.skywalking.oap.server.starter.OAPServerStartUp\n3、进行初始化操作\n4、moduleManager#init方法调用BootstrapFlow#start方法，BootstrapFlow#start调用ModuleProvider#start方法\n5、抽象类ModuleProvider#start方法实现类JDBCStorageProvider#start方法，此方法开始调用 modelInstaller#start方法用于重写列，使语法兼容MySQL，然后调用StorageModels#addModelListener方法用于把创建或修改表的操作通知到ModelInstaller#whenCreating\n6、收到通知后，ModelInstaller#whenCreating开始建表操作\n7、方法内部依次进行 1.创建/更新 表字段；2.添加/更新 字段索引；3.创建/更新 关联表的字段及索引\n8、为表添加ID字段、普通字段；添加字段类型及长度；执行建表SQL\n9、添加字段类型，进行字段长度转换\np{text-indent:2em}\n","date":"2023-04-14T00:00:00Z","permalink":"/p/skywalking%E8%87%AA%E5%8A%A8%E5%BB%BA%E8%A1%A8-%E9%80%BB%E8%BE%91%E6%A2%B3%E7%90%86skywalking-automatic-table-creation-logical-sorting/","title":"skyWalking自动建表-逻辑梳理(SkyWalking automatic table creation - logical sorting)"},{"content":"JAVA generated itinerary PDF 一、pom依赖 首先引入PDF需要的pom依赖\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;!-- pdf--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.itextpdf\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;itextpdf\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.5.13\u0026lt;/version\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.itextpdf\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;itext-asian\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.2.0\u0026lt;/version\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; 二、PDF操作工具类及行程单实体类 创建PDF操作工具类及行程单实体类，调用 PDFUtil.createPDFFile()方法创建行程单。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 /** * PDF操作工具类 */ public class PDFUtil { private static Logger logger = LoggerFactory.getLogger(PDFUtil.class); public static final String PDF_FILE_SUFFIX = \u0026#34;.pdf\u0026#34;; public static Font headFont; public static Font keyFont; // 标题 public static Font titleFont; // 设置字体大小 public static Font textFont; public static final int MAX_WIDTH = 520; static { //中文格式 BaseFont bfChinese; try { // 设置中文显示 bfChinese = BaseFont.createFont(\u0026#34;STSong-Light\u0026#34;, \u0026#34;UniGB-UCS2-H\u0026#34;, BaseFont.NOT_EMBEDDED); headFont = new Font(bfChinese, 10, java.awt.Font.BOLD); keyFont = new Font(bfChinese, 8, java.awt.Font.BOLD); textFont = new Font(bfChinese, 8, Font.NORMAL); titleFont = new Font(bfChinese, 16, Font.NORMAL); } catch (Exception e) { logger.error(\u0026#34;创建pdf格式失败，异常={}\u0026#34;, e); throw new RuntimeException(e); } } /** * 为表格添加一个内容 * * @param value 值 * @param font 字体 * @param align 对齐方式 * @return 添加的文本框 */ public static PdfPCell createCell(String value, Font font, int align) { PdfPCell cell = new PdfPCell(); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(align); font.setColor(BaseColor.WHITE); cell.setPhrase(new Phrase(value, font)); cell.setBackgroundColor(BaseColor.GRAY); return cell; } /** * 为表格添加一个内容 * * @param value 值 * @param font 字体 * @return 添加的文本框 */ public static PdfPCell createCell(String value, Font font) { PdfPCell cell = new PdfPCell(); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setPhrase(new Phrase(value, font)); return cell; } /** * 为表格添加一个内容 * * @param value 值 * @param font 字体 * @param align 对齐方式 * @param colspan 占多少列 * @return 添加的文本框 */ public static PdfPCell createCell(String value, Font font, int align, int colspan) { PdfPCell cell = new PdfPCell(); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(align); cell.setColspan(colspan); cell.setPhrase(new Phrase(value, font)); return cell; } /** * 为表格添加一个内容 * * @param value 值 * @param font 字体 * @param align 对齐方式 * @param colspan 占多少列 * @param boderFlag 是否有有边框 * @return 添加的文本框 */ public static PdfPCell createCell(String value, Font font, int align, int colspan, boolean boderFlag) { PdfPCell cell = new PdfPCell(); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(align); cell.setColspan(colspan); cell.setPhrase(new Phrase(value, font)); cell.setPadding(3.0f); if (!boderFlag) { cell.setBorder(0); cell.setPaddingTop(4.0f); cell.setPaddingBottom(8.0f); } return cell; } /** * 创建一个表格对象 * * @param colNumber 表格的列数 * @return 生成的表格对象 */ public static PdfPTable createTable(int colNumber) { PdfPTable table = new PdfPTable(colNumber); table.setTotalWidth(MAX_WIDTH); table.setLockedWidth(true); table.setHorizontalAlignment(Element.ALIGN_CENTER); table.getDefaultCell().setBorder(1); return table; } public PdfPTable createTable(float[] widths) { PdfPTable table = new PdfPTable(widths); try { table.setTotalWidth(MAX_WIDTH); table.setLockedWidth(true); table.setHorizontalAlignment(Element.ALIGN_CENTER); table.getDefaultCell().setBorder(1); } catch (Exception e) { e.printStackTrace(); } return table; } public static PdfPTable createBlankTable() { PdfPTable table = new PdfPTable(1); table.getDefaultCell().setBorder(0); table.addCell(createCell(\u0026#34;\u0026#34;, keyFont)); table.setSpacingAfter(20.0f); table.setSpacingBefore(20.0f); return table; } /** * 提供外界调用的接口，生成以head为表头，list为数据的pdf * * @param fileName 文件名 * @param titleName 表单名 * @param headName 数据表头 * @param headField 对应类的属性名 * @param list 数据 */ public static \u0026lt;T\u0026gt; File generatePDFs(String fileName, String titleName, String[] headName, String[] headField, List\u0026lt;T\u0026gt; list) { // 建立一个Document对象 Document document = new Document(); // 设置页面大小 document.setPageSize(PageSize.A4); File tempFile = null; try { tempFile = File.createTempFile(fileName, PDF_FILE_SUFFIX); PdfWriter.getInstance(document, new FileOutputStream(tempFile)); document.open(); } catch (Exception e) { e.printStackTrace(); } Class classType = list.get(0).getClass(); int colNum = headName.length; PdfPTable table = createTable(colNum); // 添加标题 table.addCell(createCell(titleName, titleFont, Element.ALIGN_CENTER, colNum, false)); //设置表头 for (int i = 0; i \u0026lt; colNum; i++) { table.addCell(createCell(headName[i], keyFont, Element.ALIGN_CENTER)); } for (int i = 0; i \u0026lt; list.size(); i++) { T t = list.get(i); for (int j = 0; j \u0026lt; colNum; j++) { //获得get方法,getName,getAge等 String getMethodName = \u0026#34;get\u0026#34; + headField[j].substring(0, 1).toUpperCase() + headField[j].substring(1); try { //通过反射获得相应的get方法，用于获得相应的属性值 Method method = classType.getMethod(getMethodName, new Class[]{}); logger.debug(getMethodName + \u0026#34;:\u0026#34; + method.invoke(t, new Class[]{}) + \u0026#34;,\u0026#34;); //添加数据 table.addCell(createCell(method.invoke(t, new Class[]{}).toString(), textFont)); } catch (Exception e) { logger.error(\u0026#34;属性设值错误：入参={},异常={}\u0026#34;, JSON.toJSONString(t), e); throw new RuntimeException(e); } } } try { //将表格添加到文档中 document.add(table); } catch (Exception e) { logger.error(\u0026#34;\u0026#34;); throw new RuntimeException(e); } //关闭流 document.close(); return tempFile; } /** * 构建行程单PDF文件 * @param date 构建日期 * @param phone 手机号 * @param baseTripInfos 行程信息 * @param outputStream 输出流 */ public static void createPDFFile(Date date, String phone, List\u0026lt;BaseTripInfo\u0026gt; baseTripInfos, OutputStream outputStream) { String[] header = new String[]{\u0026#34;序号\u0026#34;, \u0026#34;出站时间\u0026#34;, \u0026#34;入站站点\u0026#34;, \u0026#34;出站站点\u0026#34;, \u0026#34;金额（元）\u0026#34;}; int[] width = new int[]{20, 45, 35, 35, 35}; Document document = null; try { // 建立一个Document对象 document = new Document(); // 设置页面大小 document.setPageSize(PageSize.A4); PdfWriter.getInstance(document, outputStream); document.open(); // 设置列以及列宽度 PdfPTable table = PDFUtil.createTable(header.length); table.setWidths(width); // 添加标题以及空行 table.addCell(PDFUtil.createCell(\u0026#34;广州地铁—行程单\u0026#34;, PDFUtil.titleFont, Element.ALIGN_CENTER, header.length, false)); table.addCell(PDFUtil.createCell(\u0026#34; \u0026#34;, PDFUtil.titleFont, Element.ALIGN_CENTER, header.length, false)); // 设置申请日期以及行程起止日期 table.addCell(PDFUtil.createCell(\u0026#34;申请时间：\u0026#34; + cn.hutool.core.date.DateUtil.format(date, \u0026#34;yyyy-MM-dd\u0026#34;), PDFUtil.textFont, Element.ALIGN_LEFT, 2, false)); table.addCell(PDFUtil.createCell(\u0026#34;行程人手机号：\u0026#34; + phone, PDFUtil.textFont, Element.ALIGN_LEFT, 2, false)); // 计算该行程单总金额 double totalAmount = 0; for (BaseTripInfo baseTripInfo : baseTripInfos) { totalAmount += Double.parseDouble(baseTripInfo.getPrice()); } table.addCell(PDFUtil.createCell(\u0026#34;共\u0026#34; + baseTripInfos.size() + \u0026#34;笔行程, 合计 \u0026#34; + NumberUtil.round(totalAmount, 2).toString() + \u0026#34;元\u0026#34;, PDFUtil.textFont, Element.ALIGN_LEFT, header.length, false)); // 设置表头 for (String headName : header) { table.addCell(PDFUtil.createCell(headName, PDFUtil.keyFont, Element.ALIGN_CENTER)); } for (int i = 0; i \u0026lt; baseTripInfos.size(); i++) { // 序号 table.addCell(createTextCell(String.valueOf(i + 1))); // 出站时间 table.addCell(createTextCell(baseTripInfos.get(i).getExitDate() == null ? \u0026#34;\u0026#34; : baseTripInfos.get(i).getExitDate())); // 进站站点 table.addCell(createTextCell(baseTripInfos.get(i).getEnterStation() == null ? \u0026#34;\u0026#34; : baseTripInfos.get(i).getEnterStation())); // 出站站点 table.addCell(createTextCell(baseTripInfos.get(i).getExitStation() == null ? \u0026#34;\u0026#34; : baseTripInfos.get(i).getExitStation())); // 扣费金额 double price = Double.parseDouble(baseTripInfos.get(i).getPrice()); table.addCell(createTextCell(NumberUtil.round(price, 2).toString())); } //将表格添加到文档中 document.add(table); } catch (DocumentException e) { logger.error(\u0026#34;Document添加PdfWriter实例失败\u0026#34;, e); } finally { //关闭流 if (document != null) { document.close(); } } } private static PdfPCell createTextCell(String value) { return PDFUtil.createCell(value, PDFUtil.textFont); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 /** * 行程单信息 */ public class BaseTripInfo { /** * 行程ID */ private String tripId; /** * 入站站点 */ private String enterStation; /** * 入站时间 */ private String enterDate; /** * 出站站点 */ private String exitStation; /** * 出站时间 */ private String exitDate; /** * 金额 */ private String price; /** * 设备ID */ private String deviceId; public String getTripId() { return tripId; } public void setTripId(String tripId) { this.tripId = tripId; } public String getEnterStation() { return enterStation; } public void setEnterStation(String enterStation) { this.enterStation = enterStation; } public String getEnterDate() { return enterDate; } public void setEnterDate(String enterDate) { this.enterDate = enterDate; } public String getExitStation() { return exitStation; } public void setExitStation(String exitStation) { this.exitStation = exitStation; } public String getExitDate() { return exitDate; } public void setExitDate(String exitDate) { this.exitDate = exitDate; } public String getPrice() { return price; } public void setPrice(String price) { this.price = price; } public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } @Override public String toString() { return \u0026#34;BaseTripInfo{\u0026#34; + \u0026#34;tripId=\u0026#39;\u0026#34; + tripId + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, enterStation=\u0026#39;\u0026#34; + enterStation + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, enterDate=\u0026#39;\u0026#34; + enterDate + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, exitStation=\u0026#39;\u0026#34; + exitStation + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, exitDate=\u0026#39;\u0026#34; + exitDate + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, price=\u0026#39;\u0026#34; + price + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, deviceId=\u0026#39;\u0026#34; + deviceId + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } ","date":"2023-04-11T00:00:00Z","permalink":"/p/java%E7%94%9F%E6%88%90%E8%A1%8C%E7%A8%8B%E5%8D%95pdf/","title":"JAVA生成行程单PDF"},{"content":"TOC\n背景介绍 签名/验签时，经常需要将参数名按照ASCII码从小到大排序后，然后进行加密/解密操作。这里介绍三种ASCII码从小到大排序方法。\nGuava [1] StringBuffer hutool工具类 测试方法 1 下面的测试方法都是以TreeMap存储参数后，做的排序。因为**TreeMap可以自动排序**。需要注意，Guava的字符串拼接类Joiner并没有做排序，如果用HashMap，需要先使用Arrays.sort排序。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void main(String[] args) { //签名/验签时，将参数名ASCII码从小到大排序 Map\u0026lt;String, Object\u0026gt; map = new TreeMap\u0026lt;String,Object\u0026gt;(); map.put(\u0026#34;logicId\u0026#34;, \u0026#34;logicId()\u0026#34;); map.put(\u0026#34;cardType\u0026#34;, \u0026#34;tripRecentForm.getCardType()\u0026#34;); map.put(\u0026#34;traTime\u0026#34;, \u0026#34;tripRecentForm.getTraTime()\u0026#34;); map.put(\u0026#34;deviceId\u0026#34;, \u0026#34;tripRecentForm.getDeviceId()\u0026#34;); map.put(\u0026#34;traStation\u0026#34;, \u0026#34;tripRecentForm.getTraStation()\u0026#34;); map.put(\u0026#34;version\u0026#34;, \u0026#34;tripRecentForm.getVersion()\u0026#34;); //1.Guava String content1 = Joiner.on(\u0026#34;\u0026amp;\u0026#34;).withKeyValueSeparator(\u0026#34;=\u0026#34;).join(map); System.out.println(\u0026#34;content1:\u0026#34;+content1); //2.StringBuffer StringBuffer stringBuffer = new StringBuffer(); map.forEach((key, value) -\u0026gt; stringBuffer.append(key).append(\u0026#34;=\u0026#34;).append(value).append(\u0026#34;\u0026amp;\u0026#34;)); String content2 = stringBuffer.deleteCharAt(stringBuffer.length()-1).toString(); System.out.println(\u0026#34;content2:\u0026#34;+content2); //3.hutool工具类 String content3 = MapUtil.sortJoin(map, \u0026#34;\u0026amp;\u0026#34;, \u0026#34;=\u0026#34;, true, null); System.out.println(\u0026#34;content3:\u0026#34;+content3); } 对比结果 1 2 3 4 5 6 d:\\zen_v1.0\\jdk-8u201\\bin\\java.exe content1:cardType=tripRecentForm.getCardType()\u0026amp;deviceId=tripRecentForm.getDeviceId()\u0026amp;logicId=logicId()\u0026amp;traStation=tripRecentForm.getTraStation()\u0026amp;traTime=tripRecentForm.getTraTime()\u0026amp;version=tripRecentForm.getVersion() content2:cardType=tripRecentForm.getCardType()\u0026amp;deviceId=tripRecentForm.getDeviceId()\u0026amp;logicId=logicId()\u0026amp;traStation=tripRecentForm.getTraStation()\u0026amp;traTime=tripRecentForm.getTraTime()\u0026amp;version=tripRecentForm.getVersion() content3:cardType=tripRecentForm.getCardType()\u0026amp;deviceId=tripRecentForm.getDeviceId()\u0026amp;logicId=logicId()\u0026amp;traStation=tripRecentForm.getTraStation()\u0026amp;traTime=tripRecentForm.getTraTime()\u0026amp;version=tripRecentForm.getVersion() Process finished with exit code 0 [1] Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库，提供了一些常用的便利的操作工具类，减少因为 空指针、异步操作等引起的问题BUG，提高开发效率。例如：集合 、缓存、原生类型支持 、并发库 、通用注解 、字符串处理 、I/O 等等。\n","date":"2023-04-11T00:00:00Z","permalink":"/p/%E5%8F%82%E6%95%B0%E5%90%8Dascii%E7%A0%81%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%BA%8F/","title":"参数名ASCII码从小到大排序"},{"content":"TOC\n现状及目标 项目中会有配置文件来存放敏感信息（比如数据库密码、中间件密码等），这些明文存储的敏感信息一旦泄露，会引发严重的安全事故。为了消除安全隐患，最直接的方式就是把明文敏感信息加密，在程序需要用到的时候进行解密。针对加密、解密，Jasypt 框架提供了很好的解决方案。\nJasypt - 简介 Jasypt - 是什么 JASYPT: Java Simplified Encryption 是一个Java类库，它允许开发人员以很简单的方式添加基本加密功能，且无需深入研究加密原理。\nJasypt - 简介 Jasypt是基于标准的加密技术所做的加强，不仅简化了加密和检查密码的操作，还具有高度的可配置性。开发时可以自主选择不同的加密算法、salt生成等高级功能。 GitHub地址：GitHub项目地址 官网：jasypt官网\nJasypt - 特点 Jasypt 具有安全性高、线程安全、配置性高、跨语言平台等优点。具体内容见官网。 主要特点：官网主要特点\nJasypt - 原理 后续补充。\nJasypt - 集成方式 Jasypt Spring Boot 为 Spring Boot 应用程序中的属性源提供加密支持。 有 3 种方法可以将 jasypt-spring-boot 集成到您的项目中：\n使用 @SpringBootApplication 或 @EnableAutoConfiguration 将在整个 Spring 环境中启用可加密属性，只需将 starter jar jasypt-spring-boot-starter 添加到您的类路径中 将 jasypt-spring-boot 添加到您的类路径并将 @EnableEncryptableProperties 添加到您的主配置类以在整个 Spring 环境中启用可加密的属性 将 jasypt-spring-boot 添加到您的类路径并使用 @EncrytablePropertySource 声明各个可加密的属性源 Jasypt - starter方式使用案例 引入Maven依赖 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.ulisesbocchio\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jasypt-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${jasypt-spring-boot}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; properties 配置文件修改 1 2 spring.shardingsphere.datasource.ds0.username =ENC(Pvpo02TGnqgo0KCdy2KhYssSZ9ZAvzzZ) spring.shardingsphere.datasource.ds0.password =ENC(fxXySsO/WECGD6AjNs1wDI7lWZDLdgjw) salt设置 1 jasypt.encryptor.password= xxx 启动类启用配置 1 2 3 4 5 @Configuration @EnableEncryptableProperties public class MyApplication { ... } 测试类测试 1 2 3 4 5 6 7 8 9 10 11 @Test public void jasyptTest() { BasicTextEncryptor encryptor = new BasicTextEncryptor(); encryptor.setPassword(\u0026#34;G3CvKz6pLd9\u0026#34;); String encrypted = encryptor.encrypt(\u0026#34;testuser\u0026#34;); //加密后的数据：Pvpo02TGnqgo0KCdy2KhYssSZ9ZAvzzZ System.out.println(encrypted); String decrypt = encryptor.decrypt(\u0026#34;s2SThtwAB3Cxlwc+2awSm6qe5CEpa9Fa\u0026#34;); //解密后的数据：testuser System.out.println(decrypt); } ","date":"2021-09-06T00:00:00Z","permalink":"/p/%E5%85%B3%E4%BA%8Ejayspt%E5%8A%A0%E5%AF%86%E6%98%8E%E6%96%87%E6%95%8F%E6%84%9F%E4%BF%A1%E6%81%AF%E7%9A%84%E4%BD%BF%E7%94%A8/","title":"关于Jayspt加密明文敏感信息的使用"},{"content":" 现状及目标 ShardingSphere-JDBC简介 ShardingSphere-JDBC是什么 ShardingSphere - 简介 ShardingSphere-JDBC - 优势 Sharding-JDBC 分片算法 Sharding-JDBC 分片策略 ShardingSphere-JDBC - 使用案例 (目录)\n现状及目标 由于旧的业务表中数据量会持续增长，且没有对数据做分表、索引，最终导致查询上亿数据时报错。因此决定使用ShardingSphere-JDBC对表数据做数据分片处理。本文主要介绍ShardingSphere-JDBC的主要功能、优势及用法。\nShardingSphere-JDBC简介 ShardingSphere-JDBC是什么 ShardingSphere-JDBC作为Apache ShardingSphere的一个独立的产品。定位为轻量级 Java 框架，在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库，以 jar 包形式提供服务，无需额外部署和依赖，可理解为增强版的 JDBC 驱动，完全兼容 JDBC 和各种 ORM 框架。\nShardingSphere - 简介 ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈，它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar（计划中）这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能，可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。 官网：官网 官方API：sharding-jdbc手册\nShardingSphere-JDBC - 优势 Sharding-JDBC的优势在于对Java应用的友好度。 主要功能\n区别\nSharding-JDBC 分片算法 通过分片算法将数据分片，支持通过=、\u0026gt;=、\u0026lt;=、\u0026gt;、\u0026lt;、BETWEEN和IN分片。分片算法需要应用方开发者自行实现，可实现的灵活度非常高。 目前提供4种分片算法。\n精确分片算法 对应PreciseShardingAlgorithm，用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。 范围分片算法 对应RangeShardingAlgorithm，用于处理使用单一键作为分片键的BETWEEN AND、\u0026gt;、\u0026lt;、\u0026gt;=、\u0026lt;=进行分片的场景。需要配合StandardShardingStrategy使用。 复合分片算法 对应ComplexKeysShardingAlgorithm，用于处理使用多键作为分片键进行分片的场景，包含多个分片键的逻辑较复杂，需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。 Hint分片算法 对应HintShardingAlgorithm，用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。 Sharding-JDBC 分片策略 包含分片键和分片算法，由于分片算法的独立性，将其独立抽离。真正可用于分片操作的是分片键 + 分片算法，也就是分片策略。目前提供5种分片策略。\n标准分片策略 对应StandardShardingStrategy。提供对SQL语句中的=, \u0026gt;, \u0026lt;, \u0026gt;=, \u0026lt;=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键，提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的，用于处理=和IN的分片。RangeShardingAlgorithm是可选的，用于处理BETWEEN AND, \u0026gt;, \u0026lt;, \u0026gt;=, \u0026lt;=分片，如果不配置RangeShardingAlgorithm，SQL中的BETWEEN AND将按照全库路由处理。 复合分片策略 对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, \u0026gt;, \u0026lt;, \u0026gt;=, \u0026lt;=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键，由于多分片键之间的关系复杂，因此并未进行过多的封装，而是直接将分片键值组合以及分片操作符透传至分片算法，完全由应用开发者实现，提供最大的灵活度。 行表达式分片策略 对应InlineShardingStrategy。使用Groovy的表达式，提供对SQL语句中的=和IN的分片操作支持，只支持单分片键。对于简单的分片算法，可以通过简单的配置使用，从而避免繁琐的Java代码开发，如: t_user_$-\u0026gt;{u_id % 8} 表示t_user表根据u_id模8，而分成8张表，表名称为t_user_0到t_user_7。 Hint分片策略 对应HintShardingStrategy。通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。 不分片策略 对应NoneShardingStrategy。不分片的策略。 ShardingSphere-JDBC - 使用案例 引入Maven依赖 1 2 3 4 5 6 7 8 9 10 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;sharding-jdbc-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.sharding-jdbc}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;sharding-jdbc-spring-namespace\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.sharding-jdbc}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 基于springboot properties的配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ############################### sharding-jdbc相关配置 ############################# #配置数据源 spring.shardingsphere.datasource.names =ds0 spring.shardingsphere.datasource.ds0.type =com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.ds0.url =jdbc:mysql://127.0.0.1:3311/sharding-test?serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;useUnicode=true\u0026amp;characterEncoding=UTF-8\u0026amp;allowMultiQueries=true\u0026amp;autoReconnect=true\u0026amp;failOverReadOnly=false\u0026amp;useAffectedRows=true spring.shardingsphere.datasource.ds0.username =ENC(s2SThtwAB3Cxlwc+2awSm6qe5CEpa9Fa) spring.shardingsphere.datasource.ds0.password =ENC(Fu/RreOI95TySDuYErAG9ucleo9jY/Wv) spring.shardingsphere.datasource.ds0.driver-class-name =com.mysql.jdbc.Driver spring.shardingsphere.props.sql.show =true #连接池初始化连接数 spring.shardingsphere.datasource.ds0.initial-size=5 #连接池最大连接数 spring.shardingsphere.datasource.ds0.max-active=30 #连接池最小连接数 spring.shardingsphere.datasource.ds0.min-idle=5 #获取连接时最大等待时间，单位毫秒 spring.shardingsphere.datasource.ds0.max-wait=60000 #配置间隔多久才进行一次检测，检测需要关闭的空闲连接，单位是毫秒 spring.shardingsphere.datasource.ds0.time-between-eviction-runs-millis=60000 #连接保持空闲而不被驱逐的最小时间 spring.shardingsphere.datasource.ds0.min-evictable-idle-time-millis=300000 #用来检测连接是否有效的sql，要求是一个查询语句 spring.shardingsphere.datasource.ds0.validation-query=SELECT 1 FROM DUAL #建议配置为true，不影响性能，并且保证安全性。申请连接的时候检测，如果空闲时间大于timeBetweenEvictionRunsMillis，执行validationQuery检测连接是否有效。 spring.shardingsphere.datasource.ds0.test-while-idle=true #申请连接时执行validationQuery检测连接是否有效，做了这个配置会降低性能。 spring.shardingsphere.datasource.ds0.test-on-borrow=false #归还连接时执行validationQuery检测连接是否有效，做了这个配置会降低性能。 spring.shardingsphere.datasource.ds0.test-on-return=false #是否缓存preparedStatement，也就是PSCache。PSCache对支持游标的数据库性能提升巨大，比如说oracle。在mysql下建议关闭。 spring.shardingsphere.datasource.ds0.pool-prepared-statements=true #要启用PSCache，必须配置大于0，当大于0时，poolPreparedStatements自动触发修改为true。 spring.shardingsphere.datasource.ds0.max-pool-prepared-statement-per-connection-size=50 #配置监控统计拦截的filters，去掉后监控界面sql无法统计 spring.shardingsphere.datasource.ds0.filters=stat,wall #通过connectProperties属性来打开mergeSql功能；慢SQL记录 spring.shardingsphere.datasource.ds0.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 #合并多个DruidDataSource的监控数据 spring.shardingsphere.datasource.ds0.use-global-data-source-stat=true spring.shardingsphere.sharding.tables.trip_ycym_record.actual-data-nodes=ds0.trip_ycym_record_$-\u0026gt;{2020..2023}${(1..12).collect{t -\u0026gt;t.toString().padLeft(2,\u0026#39;0\u0026#39;)}} spring.shardingsphere.sharding.tables.trip_ycym_record.table-strategy.standard.sharding-column=paytime #精确分片算法类名称，用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器 spring.shardingsphere.sharding.tables.trip_ycym_record.table-strategy.standard.precise-algorithm-class-name=com.vamdawn.config.sharding.TripPreciseShardingAlgorithm #范围分片算法类名称，用于BETWEEN，可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器 spring.shardingsphere.sharding.tables.trip_ycym_record.table-strategy.standard.range-algorithm-class-name=com.vamdawm.config.sharding.TripRangeShardingAlgorithm PreciseShardingAlgorithm：用于保存数据时 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Slf4j public class TripPreciseShardingAlgorithm implements PreciseShardingAlgorithm\u0026lt;String\u0026gt; { @Override public String doSharding(Collection\u0026lt;String\u0026gt; availableTargetNames, PreciseShardingValue\u0026lt;String\u0026gt; shardingValue) { log.info(\u0026#34;availableTargetNames : {}\u0026#34;, availableTargetNames); log.info(\u0026#34;shardingValue:{}\u0026#34;, JSON.toJSONString(shardingValue)); //获取paytime格式化后的年份月份(2108) Date paytime = DateUtil.strBeauty2Date(shardingValue.getValue()); String payTimeFormat = DateUtil.date2LocalDateTime(paytime).format(DateTimeFormatter.ofPattern(\u0026#34;yyyyMM\u0026#34;)); for (String tableName : availableTargetNames) { String tableNameFormat = tableName.substring(tableName.lastIndexOf(\u0026#34;_\u0026#34;) + 1); if (ObjectUtils.nullSafeEquals(payTimeFormat, tableNameFormat)) { log.info(\u0026#34;返回最终配置的表名====={}\u0026#34;, tableName); return tableName; } } throw new IllegalArgumentException(); } } RangeShardingAlgorithm：用于根据时间段查询数据时 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Slf4j public class TripRangeShardingAlgorithm implements RangeShardingAlgorithm\u0026lt;String\u0026gt; { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(\u0026#34;yyyyMM\u0026#34;); private static final String TABLENAMEPREFIX = \u0026#34;trip_sharding_record_\u0026#34;; private static final String TIMEFORMATTER = \u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;; @Override public Collection\u0026lt;String\u0026gt; doSharding(Collection\u0026lt;String\u0026gt; availableTargetNames, RangeShardingValue\u0026lt;String\u0026gt; shardingValue) { log.info(\u0026#34;目标表名集合availableTargetNames : {}\u0026#34;, availableTargetNames); log.info(\u0026#34;shardingValue:{}\u0026#34;, JSON.toJSONString(shardingValue)); //最终要查询的实际表名集合 Collection\u0026lt;String\u0026gt; actualTableNameList = new LinkedHashSet(availableTargetNames.size()); //初始化上线日期 String initDateStr = \u0026#34;2021-05-01 00:00:00\u0026#34;; Date initDate = DateUtil.strBeauty2Date(initDateStr); Range\u0026lt;String\u0026gt; valueRange = shardingValue.getValueRange(); log.info(\u0026#34;日期范围:{}\u0026#34;, valueRange); LocalDateTime initDateTime = DateUtil.date2LocalDateTime(initDate); String lowerDateStr = valueRange.lowerEndpoint(); LocalDateTime lowerDateTime = LocalDateTimeUtil.parse(lowerDateStr, TIMEFORMATTER); //设置最早开始查询时间为上线时间 if (lowerDateTime.isBefore(initDateTime)) { lowerDateTime = initDateTime; } String upperDateStr = valueRange.upperEndpoint(); LocalDateTime upperDateTime = LocalDateTimeUtil.parse(upperDateStr, TIMEFORMATTER); //获取到相差的月份，计算出之间每个月份表 long intervalMonth = lowerDateTime.until(upperDateTime, ChronoUnit.MONTHS); log.info(\u0026#34;获取到相差的月份:{}\u0026#34;, intervalMonth); String tableNameMonth = TABLENAMEPREFIX + lowerDateTime.format(FORMATTER); for (long i = 0; i \u0026lt;= intervalMonth; i++) { String monthFormat = lowerDateTime.plusMonths(i).format(FORMATTER); tableNameMonth = TABLENAMEPREFIX + monthFormat; actualTableNameList.add(tableNameMonth); } log.info(\u0026#34;最终要查询的实际表名集合为:{}\u0026#34;, actualTableNameList); return actualTableNameList; } } ","date":"2021-08-31T00:00:00Z","permalink":"/p/%E5%85%B3%E4%BA%8Eshardingsphere-jdbc%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/","title":"关于ShardingSphere-JDBC的简单使用"},{"content":" what is SnowflakeId why is SnowflakeId how to do note (目录)\nwhat is SnowflakeId 随着服务化的演进，服务越来越多，数据库越分越细，有时候一个业务也会用到多个数据库。这时，使用传统的主键自增或者UUID（无序，长度过长）方式就会产生id重复，不能满足使用场景。分布式系统中为了保证id唯一，就需要全局的唯一id生成策略。\n雪花算法优点： 生成的ID不重复 生成性能高（每向数据库插入一条数据不用进行重新排列） 基于时间戳，可以基本保证有序递增 雪花算法存在的问题： 时间回拨问题：由于机器的时间是动态的调整的，有可能会出现时间跑到之前几毫秒，如果这个时候获取到了这种时间，则会出现数据重复 机器id分配及回收问题：目前机器id需要每台机器不一样，这样的方式分配需要有方案进行处理，同时也要考虑，如果改机器宕机了，对应的workerId分配后的回收问题 机器id上限：机器id是固定的bit，那么也就是对应的机器个数是有上限的，在有些业务场景下，需要所有机器共享同一个业务空间，那么10bit表示的1024台机器是不够的。\nwhy is SnowflakeId snowflake生成的ID整体上按照时间自增排序，并且整个分布式系统内不会产生ID碰撞（由datacenter和workerId作区分），并且效率较高。据说snowflake每秒能够产生26万个ID。\nhow to do snowflake的结构如下(每部分用-分开): 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000\n第一位为符号位不使用，接下来的41位为毫秒级时间(41位的长度可以使用69年)， 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点） ，(5+5,0+10可以调整) 最后12位是毫秒内的计数（12位的计数顺序号支持每个节点每毫秒产生4096个ID序号） 一共加起来刚好64位，为一个Long型。\njava中long类型占8字节，1字节=8位（1byte=8bit）也就是64位，每一位都有0和1两种状态，64位也就可以表示264个状态，也就是264个数，而long类型是有符号的（分正负），负数用-263至-1表示，正数用0至263-1表示，加起来正是2^64个数 long MAX_VALUE = -1L ^ (-1L \u0026lt;\u0026lt; 63)= 9223372036854775807L（最大长度19位）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 /** * Snowflake 基于雪花算法的ID生成器 */ public class SnowflakeIdWorker { //定义起始时间：一般地，选用系统上线的时间2021-01-01 00:00:00 private final long startTime = 1609430400000L; //序列号位数 private final long sequenceBits = 12L; //机器ID位数 private final long workerIdBits = 5L; //数据中心ID位数 private final long datacenterIdBits = 5L; //序列号最大值, 4095 private final long sequenceMask = -1L ^ (-1L \u0026lt;\u0026lt; sequenceBits); //机器ID最大值, 31 private final long maxWorkerId = -1L ^ (-1L \u0026lt;\u0026lt; workerIdBits); //数据中心ID最大值, 31 private final long maxDatacenterId = -1L ^ (-1L \u0026lt;\u0026lt; datacenterIdBits); //机器ID左移位数, 12 private final long workerIdShift = sequenceBits; //数据中心ID左移位数, 12+5 private final long datacenterIdShift = sequenceBits + workerIdBits; //时间戳左移位数, 12+5+5 private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private long workerId; private long datacenterId; //相同毫秒内的序列号, Value Range: [0,4095] private long sequence = 0L; //上一次生成id使用的时间戳 private long lastTimestamp = -1L; public SnowflakeIdWorker(long workerId, long datacenterId) { if (workerId \u0026lt;= 31L \u0026amp;\u0026amp; workerId \u0026gt;= 0L) { if (datacenterId \u0026lt;= 31L \u0026amp;\u0026amp; datacenterId \u0026gt;= 0L) { this.workerId = workerId; this.datacenterId = datacenterId; } else { throw new IllegalArgumentException(String.format(\u0026#34;datacenter Id can\u0026#39;t be greater than %d or less than 0\u0026#34;, maxDatacenterId)); } } else { throw new IllegalArgumentException(String.format(\u0026#34;worker Id can\u0026#39;t be greater than %d or less than 0\u0026#34;, maxWorkerId)); } } public SnowflakeIdWorker() { } /** * 获取ID * * @return */ public synchronized long nextId() { long timestamp = this.timeGen(); //当前时间小于上一次生成ID的时间戳，系统时钟被回 if (timestamp \u0026lt; this.lastTimestamp) { throw new RuntimeException(String.format(\u0026#34;Clock moved backwards. Refusing to generate id for %d milliseconds\u0026#34;, this.lastTimestamp - timestamp)); } else { //当前时间等于上一次生成ID的时间戳,则通过序列号来区分 if (this.lastTimestamp == timestamp) { //通过序列号掩码实现只取 (sequence+1) 的低12位结果，其余位全部清零 this.sequence = this.sequence + 1L \u0026amp; sequenceMask; //该时间戳下的序列号已经溢出 if (this.sequence == 0L) { //阻塞等待下一个毫秒,并获取新的时间戳 timestamp = this.tilNextMillis(this.lastTimestamp); } } else { //当前时间大于上一次生成ID的时间戳,重置序列号 this.sequence = 0L; } //更新上次时间戳信息 this.lastTimestamp = timestamp; //生成此次ID return timestamp - startTime \u0026lt;\u0026lt; timestampLeftShift | this.datacenterId \u0026lt;\u0026lt; datacenterIdShift | this.workerId \u0026lt;\u0026lt; workerIdShift | this.sequence; } } /** * 阻塞等待,直到获取新的时间戳(下一个毫秒) * * @param lastTimestamp * @return */ protected long tilNextMillis(long lastTimestamp) { long timestamp; for (timestamp = this.timeGen(); timestamp \u0026lt;= lastTimestamp; timestamp = this.timeGen()) { } return timestamp; } protected long timeGen() { return System.currentTimeMillis(); } public long getWorkerId() { return this.workerId; } public void setWorkerId(long workerId) { this.workerId = workerId; } public long getDatacenterId() { return this.datacenterId; } public void setDatacenterId(long datacenterId) { this.datacenterId = datacenterId; } public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0L, 0L); for (int i = 0; i \u0026lt; 10000000; ++i) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } } note 计算X位Bit能表示的最大值 计算X位Bit能表示的最大值，最简单的是 Math.pow(2,X)-1，不过还可以通过位运算来提高速度，即 -1^(-1\u0026lt;\u0026lt;X)。 在计算机的二进制下 -1 使用全1进行表示 这里以X等于3为例作图解说明\n41位时间戳可以使用多久\n","date":"2021-06-29T00:00:00Z","permalink":"/p/%E9%9B%AA%E8%8A%B1%E7%AE%97%E6%B3%95%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%90%86%E5%BF%B5%E5%92%8C%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/","title":"雪花算法的基本理念和简单实现"},{"content":"[toc]\n官方API 官方文档：https://mp.baomidou.com/guide/ 具体位置：https://mp.baomidou.com/guide/wrapper.html#%E7%94%A8%E6%B3%A8%E8%A7%A3\n使用概述 Mapper层方法上添加 ${ew.customSqlSegment}和@Param(Constants.WRAPPER)； 查询vo添加对应的查询条件字段，结果vo添加所想要展示的字段； service层方法中构造相应的QueryWrapper。 即可实现多表联查、动态条件查询。\n具体写法 Mapper写法： 1 2 3 @Select(\u0026#34;SELECT * FROM tableA a LEFT JOIN tableB b on a.key = b.key ${ew.customSqlSegment}\u0026#34;) List method1(@Param(Constants.WRAPPER) QueryWrapper wrapper); IPage method2(Page\u0026lt;\u0026gt; page, @Param(Constants.WRAPPER) QueryWrapper wrapper); 需要注意：wrapper不能为null，可以用new QueryWrapper\u0026lt;\u0026gt;();\nentity写法： 查询model中，如果既有A表参数，又有B表参数，需要在entity中添加字段 返回结果vo中，和A、B表对应上的字段都会自动赋值\nservice写法： 封装wrapper时，column字段最好写明表名。例：wrapper.eq(StringUtils.isNotBlank(\u0026quot;xxx\u0026quot;), \u0026quot;A.column\u0026quot;,\u0026quot;value\u0026quot;);\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Override public void getRecord() { //返回值为list QueryWrapper\u0026lt;PassRecord\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); wrapper.eq(StringUtils.isNotBlank(\u0026#34;\u0026#34;), \u0026#34;user_name\u0026#34;,\u0026#34;aaa\u0026#34;); wrapper.eq(\u0026#34;p.card_id\u0026#34;,\u0026#34;44520xxx\u0026#34;); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(\u0026#34;yyyyMMdd HH:mm:ss\u0026#34;); LocalDateTime dateTime = LocalDateTime.parse(\u0026#34;20210611 18:04:00\u0026#34;, dateTimeFormatter); Date startDate = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant()); LocalDateTime dateTime2 = LocalDateTime.parse(\u0026#34;20210615 18:04:00\u0026#34;, dateTimeFormatter); Date endDate = Date.from(dateTime2.atZone(ZoneId.systemDefault()).toInstant()); wrapper.between(\u0026#34;p.create_time\u0026#34;,startDate, endDate); List\u0026lt;PassRecord\u0026gt; list = passRecordMapper.getRecordParam(wrapper); list.forEach(passRecord -\u0026gt; { System.out.println(passRecord.getUserName() +\u0026#34;==\u0026#34;+ passRecord.getWorkPlace()); }); //返回page对象 Page\u0026lt;PassInfoDto\u0026gt; page = new Page\u0026lt;\u0026gt;(); page.setCurrent(1L); page.setSize(2L); QueryWrapper\u0026lt;PassRecord\u0026gt; queryWrapper = new QueryWrapper\u0026lt;\u0026gt;(); List\u0026lt;PassRecord\u0026gt; recordList = passRecordMapper.getRecordPageParam(page, queryWrapper); recordList.forEach(record -\u0026gt; { System.out.println(record.getUserName() + \u0026#34;\u0026#34; + record.getRecordId()); }); IPage\u0026lt;PassRecord\u0026gt; iPage = passRecordMapper.getRecordParamToPage(page, queryWrapper); System.out.println(JSON.toJSONString(iPage)); } 日志报错 wrapper不能为null\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e207103] was not registered for synchronization because synchronization is not active Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e207103] 2021-06-16 08:57:19.297 | ERROR | | http-nio-8080-exec-1 | c.g.v.c.e.ExceptionCatch:160:exception - catch Exception:org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression \u0026#39;ew.customSqlSegment\u0026#39;. Cause: org.apache.ibatis.ognl.OgnlException: source is null for getProperty(null, \u0026#34;customSqlSegment\u0026#34;) at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440) at com.sun.proxy.$Proxy110.selectList(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:223) at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:173) at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:78) at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148) at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89) at com.sun.proxy.$Proxy121.getRecordPageParam(Unknown Source) at com.grg.virusControl.mgr.service.impl.PassMgrServiceImpl.getRecord(PassMgrServiceImpl.java:316) at com.grg.virusControl.mgr.service.impl.PassMgrServiceImpl$$FastClassBySpringCGLIB$$a74fa41e.invoke(\u0026lt;generated\u0026gt;) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:685) at com.grg.virusControl.mgr.service.impl.PassMgrServiceImpl$$EnhancerBySpringCGLIB$$63af0f69.getRecord(\u0026lt;generated\u0026gt;) at com.grg.virusControl.mgr.controller.PassMgrController.getRecord(PassMgrController.java:59) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:798) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) 结果截图 ","date":"2021-06-16T00:00:00Z","permalink":"/p/mybatisplus-%E6%B3%A8%E8%A7%A3%E6%96%B9%E5%BC%8F%E5%AE%9E%E7%8E%B0%E5%A4%9A%E8%A1%A8%E5%85%B3%E8%81%94%E6%9F%A5%E8%AF%A2/","title":"MyBatisPlus 注解方式实现多表关联查询"},{"content":"TOC{:toc}\n一.基础知识 1.集合类：List和Set比较，各自的子类比较（ArrayList，Vector，LinkedList；HashSet，TreeSet）； 1 2 3 4 5 6 7 8 9 10 11 12 13 ArrayList，LinkedList，Vector都属于List List：元素是有顺序的，元素可以重复因为每个元素有自己的角标（索引） |-- ArrayList:底层的数据结构是数组结构，特点是：查询很快，增 删 稍微慢点，线程不同步 |-- LinkedList：底层使用的是链表数据结构，特点是：增 删很快，查询慢。 |--Vector:底层是数组数据结构，线程同步，被ArrayList代替了，现在用的只有他的枚举。 Set：元素是无序的，且不可以重复（存入和取出的顺序不一定一致），线程不同步。 |--HashSet：底层是哈希表数据结构。根据hashCode和equals方法来确定元素的唯一性 |--TreeSet：可以对Set集合中的元素进行排序（自然循序），底层的数据结构是二叉树， 也可以自己写个类实现Comparable 或者 Comparator 接口，定义自己的比较器，将其作为参数传递给TreeSet的构造函数。 Map：这个集合是存储键值对的，一对一对往里存，而且要确保键的唯一性（01，张三）这样的形式打印出来就是 01=张三 |--HashTable：底层是哈希表数据结构，不可以存入null键和null值，该集合线程是同步的，效率比较低。出现于JDK1.0 |--HashMap：底层是哈希表数据结构，可以存入null键和null值，线程不同步，效率较高，代替了HashTable，出现于JDK 1.2 |--TreeMap:底层是二叉树数据结构，线程不同步，可以用于个map集合中的键进行排序 2.HashMap的底层实现，之后会问ConcurrentHashMap的底层实现； 1 2 3 4 5 6 7 HashMap由数组和链表来实现对数据的存储 HashMap采用Entry数组来存储key-value对，每一个键值对组成了一个Entry实体，Entry类实际上是一个单向的链表结构，它具有Next指针，可以连接下一个Entry实体，以此来解决Hash冲突的问题。 数组存储区间是连续的，占用内存严重，故空间复杂的很大。但数组的二分查找时间复杂度小，为O(1)；数组的特点是：寻址容易，插入和删除困难； 链表存储区间离散，占用内存比较宽松，故空间复杂度很小，但时间复杂度很大，达O（N）。链表的特点是：寻址困难，插入和删除容易。 JDK 1.8的 改变：HashMap采用数组+链表+红黑树实现。 改变的地方,数据结构的存储由数组+链表的方式，变化为数组+链表+红黑树的存储方式，当链表长度超过阈值（8）时，将链表转换为红黑树。在性能上进一步得到提升。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Hashtable的同步会锁住整个数组。在高并发的情况下，性能会非常差，Java5中引入了java.util.concurrent.ConcurrentHashMap作为高吞吐量的线程安全HashMap实现，它采用了锁分离的技术允许多个修改操作并发进行 ConcurrentHashMap所使用的锁分段技术，首先将数据分成一段一段的存储，然后给每一段数据配一把锁，当一个线程占用锁访问其中一个段数据的时候，其他段的数据也能被其他线程访问。 ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock，在ConcurrentHashMap里扮演锁的角色，HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组，Segment的结构和HashMap类似，是一种数组和链表结构， 一个Segment里包含一个HashEntry数组，每个HashEntry是一个链表结构的元素， 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时，必须首先获得它对应的Segment锁。 Segment的get操作实现非常简单和高效。先经过一次再哈希，然后使用这个哈希值通过哈希运算定位到segment，再通过哈希算法定位到元素。get操作的高效之处在于整个get过程不需要加锁，除非读到的值是空的才会加锁重读，我们知道HashTable容器的get方法是需要加锁的，那么ConcurrentHashMap的get操作是如何做到不加锁的呢？原因是它的get方法里将要使用的共享变量都定义成volatile。 由于put方法里需要对共享变量进行写入操作，所以为了线程安全，在操作共享变量时必须得加锁。Put方法首先定位到Segment，然后在Segment里进行插入操作。 是否需要扩容。在插入元素前会先判断Segment里的HashEntry数组是否超过容量（threshold），如果超过阀值，数组进行扩容。值得一提的是，Segment的扩容判断比HashMap更恰当，因为HashMap是在插入元素后判断元素是否已经到达容量的，如果到达了就进行扩容，但是很有可能扩容之后没有新元素插入，这时HashMap就进行了一次无效的扩容 缺点：ConcurrentHashMap的size操作 https://blog.csdn.net/lin20044140410/article/details/79320587 3.如何实现HashMap顺序存储：可以参考LinkedHashMap的底层实现； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 方法一：维护一张表，存储数据插入的顺序，可以使用vector。但是如果删除数据呢 首先得在vector里面找到那个数据，再删除，而删除又要移动大量数据，性能效率很低 使用list，移动问题可以解决，但是查找数据O的时间消耗，如果删除m次，那查找数据的性能就是0 那总体性能也是0.性能还是没法接受。 方法二： 可以在hashmap里维护插入顺序的id,在value建一个存储id值，在维护一张表vector，并且id对应vector里面的值。 插入的时候，id+ = 1,hashmao.insert, vector.push_back 删除的时候，先hashmao.find(key),得到value，并且从value中得到id,通过id把对应vector值设置为无效。 更新： 删除+ 插入。 维护工作OK了，输出的时候直接输出vector里面的值就可以了，无效的就continue. 算法福再度为O 方法三： Java里面有个容器LinkedListHashMap,它能实现按照插入的顺序输出结果。 他的原理也是维护一张表，但他是链表，并且hashmap中维护指向链表的指针，这样可以快速定位链表中的元素 进行删除。 他的时间复杂度也是0，空间上比上面少些。 4.HashTable和ConcurrentHashMap的区别； 1 2 3 4 5 6 7 8 9 Hashtable所有的方法都是同步的，因此，它是线程安全的 Synchronized容器和Concurrent容器有什么区别？ 在Java语言中，多线程安全的容器主要分为两种：Synchronized和Concurrent，虽然它们都是线程安全的，但是它们在性能方面差距比较大。 Synchronized容器（同步容器）主要通过synchronized关键字来实现线程安全，在使用的时候会对所有的数据加锁。需要注意的是，由于同步容器将所有对容器状态的访问都串行化了，这样虽然保证了线程的安全性，但是这种方法的代价就是严重降低了并发性，当多个线程竞争容器时，吞吐量会严重降低。于是引入了Concurrent容器（并发容器），Concurrent容器采用了更加智能的方案，该方案不是对整个数据加锁，而是采取了更加细粒度的锁机制，因此，在大并发量的情况下，拥有更高的效率。 ———————————————— 原文链接：https://blog.csdn.net/weixin_39651041/article/details/79953811 5.String,StringBuffer和StringBuilder的区别； 1 2 3 4 String类是不可变类 和 String 类不同的是，StringBuffer 和 StringBuilder 类的对象能够被多次的修改，并且不产生新的未使用对象。 StringBuilder 类在 Java 5 中被提出，它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的（不能同步访问）。 由于 StringBuilder 相较于 StringBuffer 有速度优势，所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下，则必须使用 StringBuffer 类。 6.Object的方法有哪些：比如有wait方法，为什么会有； 1 锁可以是任意对象，所以任意对象调用方法一定定义在Object类中。 7.wait和sleep的区别，必须理解； 1 2 wait():释放资源，释放锁 sleep():释放资源，不释放锁 8.JVM的内存结构，JVM的算法； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1.方法区（Method Area） 2.堆区（Heap） 3.虚拟机栈（VM Stack） 4.本地方法栈（Native Method Stack） 5.程序计数器（Program Counter Register） 方法区存放了要加载的类的信息（如类名、修饰符等）、静态变量、构造函数、final定义的常量、类中的字段和方法等信息。方法区是全局共享的，在一定条件下也会被GC。 堆区是GC最频繁的，也是理解GC机制最重要的区域。堆区由所有线程共享，在虚拟机启动时创建。堆区主要用于存放对象实例及数组，所有new出来的对象都存储在该区域。 虚拟机栈占用的是操作系统内存，每个线程对应一个虚拟机栈，它是线程私有的，生命周期和线程一样，每个方法被执行时产生一个栈帧（Statck Frame），栈帧用于存储局部变量表、动态链接、操作数和方法出口等信息，当方法被调用时，栈帧入栈，当方法调用结束时，栈帧出栈。 局部变量表中存储着方法相关的局部变量，包括各种基本数据类型及对象的引用地址等，因此他有个特点：内存空间可以在编译期间就确定，运行时不再改变。 虚拟机栈定义了两种异常类型：StackOverFlowError(栈溢出)和OutOfMemoryError（内存溢出）。 本地方法栈用于支持native方法的执行，存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致，唯一的区别是，虚拟机栈执行Java方法，本地方法栈执行native方法。在很多虚拟机中（如Sun的JDK默认的HotSpot虚拟机），会将虚拟机栈和本地方法栈一起使用。 程序计数器是一个很小的内存区域，不在RAM上，而是直接划分在CPU上，程序猿无法操作它，它的作用是：JVM在解释字节码（.class）文件时，存储当前线程执行的字节码行号，只是一种概念模型，各种JVM所采用的方式不一样。 9.强引用，软引用和弱引用的区别； 1 2 3 4 强引用：new出来的对象都是强引用，GC无论如何都不会回收，即使抛出OOM异常。 软引用：只有当JVM内存不足时才会被回收。 弱引用：只要GC,就会立马回收，不管内存是否充足。 虚引用：它唯一的作用就是做一些跟踪记录，辅助finalize函数的使用。 10.数组在内存中如何分配； 1 2 3 4 5 Java数组的初始化，有以下两种方式， 1.静态初始化：初始化时由程序员显式指定每个数组元素的初始值，由系统决定数组长度，如： String[] names = new String[]{\u0026#34;多啦A梦\u0026#34;, \u0026#34;大雄\u0026#34;, \u0026#34;静香\u0026#34;}; 2.动态初始化：初始化时由程序员显示的指定数组的长度，由系统为数据每个元素分配初始值，如： String[] cars = new String[4]; //系统会默认给数组元素分配初始值为null 11. 12.springmvc的核心是什么，请求的流程是怎么处理的，控制反转怎么实现的； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 1.核心：SpringMVC框架是以请求为驱动，围绕Servlet设计，将请求发给控制器，然后通过模型对象，分派器来展示请求结果视图。其中核心类是DispatcherServlet，它是一个Servlet，顶层是实现的Servlet接口。 2.流程说明： （1）客户端（浏览器）发送请求，直接请求到DispatcherServlet。 （2）DispatcherServlet根据请求信息调用HandlerMapping，解析请求对应的Handler。 （3）解析到对应的Handler后，开始由HandlerAdapter适配器处理。 （4）HandlerAdapter会根据Handler来调用真正的处理器开处理请求，并处理相应的业务逻辑。 （5）处理器处理完业务后，会返回一个ModelAndView对象，Model是返回的数据对象，View是个逻辑上的View。 （6）ViewResolver会根据逻辑View查找实际的View。 （7）DispaterServlet把返回的Model传给View。 （8）通过View返回给请求者（浏览器） 3.IOC如何实现 通过DI（Dependency Injection，依赖注入）来实现的。 所谓依赖注入，其实就是给对象里的属性赋值，因为对象里有其他对象，因此就形成了依赖。Spring有4种方式来给属性赋值： 1. 构造方法注入constructor 2. set方法注入 3. 自动装配：Spring提供了自动装配的功能，简化了我们的配置，自动装配默认是不打开的。byName/byType 4. 注解@Autowired 配置了bean的id和class。 Spring中默认的bean为单实例模式，通过bean的class引用反射机制可以创建这个实例。 因此，spring框架通过反射代替我们创建好了实例并且替我们维护他们。 13.spring里面的aop的原理是什么； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 概念 切面（Aspect） ：官方的抽象定义为“一个关注点的模块化，这个关注点可能会横切多个对象”。 连接点（Joinpoint） ：程序执行过程中的某一行为。 通知（Advice） ：“切面”对于某个“连接点”所产生的动作。 切入点（Pointcut） ：匹配连接点的断言，在AOP中通知和一个切入点表达式关联。 目标对象（Target Object） ：被一个或者多个切面所通知的对象。 AOP代理（AOP Proxy） 在Spring AOP中有两种代理方式，JDK动态代理和CGLIB代理。 通知（Advice）类型 前置通知（Before advice） ：在某连接点（JoinPoint）之前执行的通知，但这个通知不能阻止连接点前的执行。ApplicationContext中在\u0026lt;aop:aspect\u0026gt;里面使用\u0026lt;aop:before\u0026gt;元素进行声明。 后通知（After advice） ：当某连接点退出的时候执行的通知（不论是正常返回还是异常退出）。ApplicationContext中在\u0026lt;aop:aspect\u0026gt;里面使用\u0026lt;aop:after\u0026gt;元素进行声明。 返回后通知（After return advice） ：在某连接点正常完成后执行的通知，不包括抛出异常的情况。ApplicationContext中在\u0026lt;aop:aspect\u0026gt;里面使用\u0026lt;after-returning\u0026gt;元素进行声明。 环绕通知（Around advice） ：包围一个连接点的通知，类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为，也可以选择不执行。ApplicationContext中在\u0026lt;aop:aspect\u0026gt;里面使用\u0026lt;aop:around\u0026gt;元素进行声明。 抛出异常后通知（After throwing advice） ： 在方法抛出异常退出时执行的通知。 ApplicationContext中在\u0026lt;aop:aspect\u0026gt;里面使用\u0026lt;aop:after-throwing\u0026gt;元素进行声明。 Spring AOP的底层都是通过代理来实现的 一种是基于JDK的动态代理 一种是基于CgLIB的动态代理 17.说说http,https协议； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 什么是HTTP? 超文本传输协议，是一个基于请求与响应，无状态的，应用层的协议，常基于TCP/IP协议传输数据，互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。 什么是HTTPS？ HTTPS是身披SSL外壳的HTTP。HTTPS是一种通过计算机网络进行安全通信的传输协议，经由HTTP进行通信，利用SSL/TLS建立全信道，加密数据包。HTTPS使用的主要目的是提供对网站服务器的身份认证，同时保护交换数据的隐私与完整性。 HTTP特点： 1.无状态：协议对客户端没有状态存储，对事物处理没有“记忆”能力，比如访问一个网站需要反复进行登录操作 2.无连接：HTTP/1.1之前，由于无状态特点，每次请求需要通过TCP三次握手四次挥手，和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源，服务器并不能区别是否已经响应过用户的请求，所以每次需要重新响应请求，需要耗费不必要的时间和流量。 3.基于请求和响应：基本的特性，由客户端发起请求，服务端响应 4.简单快速、灵活 5.通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性 HTTPS特点： 内容加密：采用混合加密技术，中间者无法直接查看明文内容 验证身份：通过证书认证客户端访问的是自己的服务器 保护数据完整性：防止传输的内容被中间人冒充或者篡改 19.osi五层网络协议； 1 五层体系结构包括：应用层、运输层、网络层、数据链路层和物理层 20.tcp，udp区别； 1 2 3 4 5 6 TCP和UDP的区别 1、基于连接与无连接;UDP是无连接的，即发送数据之前不需要建立连接 2、TCP保证数据正确性，UDP可能丢包，TCP保证数据顺序，UDP不保证。也就是说，通过TCP连接传送的数据，无差错，不丢失，不重复，且按序到达;UDP尽最大努力交付，即不保证可靠交付Tcp通过校验和，重传控制，序号标识，滑动窗口、确认应答实现可靠传输。如丢包时的重发控制，还可以对次序乱掉的分包进行顺序控制。 3、UDP具有较好的实时性，工作效率比TCP高，适用于对高速传输和实时性有较高的通信或广播通信。 4、每一条TCP连接只能是点到点的;UDP支持一对一，一对多，多对一和多对多的交互通信。 5、TCP对系统资源要求较多，UDP对系统资源要求较少。 21.用过哪些加密算法：对称加密，非对称加密算法； 1 2 3 4 5 对称加密：双方使用的同一个密钥，既可以加密又可以解密，这种加密方法称为对称加密，也称为单密钥加密。 在对称加密算法中常用的算法有：DES、AES等。 非对称加密：一对密钥由公钥和私钥组成（可以使用很多对密钥）。私钥解密公钥加密数据，公钥解密私钥加密数据（私钥公钥可以互相加密解密）。 RSA、Elgamal、背包算法、Rabin、Diffie-Hellman、ECC（椭圆曲线加密算法）。 使用最广泛的是RSA算法，Elgamal是另一种常用的非对称加密算法。 22.说说tcp三次握手，四次挥手； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 TCP三次握手和四次挥手 TCP有6种标示:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急) TCP三次握手 第一次握手 客户端向服务器发出连接请求报文，这时报文首部中的同部位SYN=1，同时随机生成初始序列号 seq=x，此时，TCP客户端进程进入了 SYN-SENT（同步已发送状态）状态。TCP规定，SYN报文段（SYN=1的报文段）不能携带数据，但需要消耗掉一个序号。这个三次握手中的开始。表示客户端想要和服务端建立连接。 第二次握手 TCP服务器收到请求报文后，如果同意连接，则发出确认报文。确认报文中应该 ACK=1，SYN=1，确认号是ack=x+1，同时也要为自己随机初始化一个序列号 seq=y，此时，TCP服务器进程进入了SYN-RCVD（同步收到）状态。这个报文也不能携带数据，但是同样要消耗一个序号。这个报文带有SYN(建立连接)和ACK(确认)标志，询问客户端是否准备好。 第三次握手 TCP客户进程收到确认后，还要向服务器给出确认。确认报文的ACK=1，ack=y+1，此时，TCP连接建立，客户端进入ESTABLISHED（已建立连接）状态。TCP规定，ACK报文段可以携带数据，但是如果不携带数据则不消耗序号。这里客户端表示我已经准备好。 思考：为什么要三次握手呢，有人说两次握手就好了 举例：已失效的连接请求报文段。 client发送了第一个连接的请求报文，但是由于网络不好，这个请求没有立即到达服务端，而是在某个网络节点中滞留了，直到某个时间才到达server，本来这已经是一个失效 的报文，但是server端接收到这个请求报文后，还是会想client发出确认的报文，表示同意连接。假如不采用三次握手，那么只要server发出确认，新的建立就连接了，但其实这个 请求是失效的请求，client是不会理睬server的确认信息，也不会向服务端发送确认的请求，但是server认为新的连接已经建立起来了，并一直等待client发来数据，这样，server的 很多资源就没白白浪费掉了，采用三次握手就是为了防止这种情况的发生，server会因为收不到确认的报文，就知道client并没有建立连接。这就是三次握手的作用。 TCP的四次挥手 第一次挥手 TCP发送一个FIN(结束)，用来关闭客户到服务端的连接。 客户端进程发出连接释放报文，并且停止发送数据。释放数据报文首部，FIN=1，其序列号为seq=u（等于前面已经传送过来的数据的最后一个字节的序号加1），此时，客户端进入FIN-WAIT-1（终止等待1）状态。 TCP规定，FIN报文段即使不携带数据，也要消耗一个序号。 第二次挥手 服务端收到这个FIN，他发回一个ACK(确认)，确认收到序号为收到序号+1，和SYN一样，一个FIN将占用一个序号。 服务器收到连接释放报文，发出确认报文，ACK=1，ack=u+1，并且带上自己的序列号seq=v，此时，服务端就进入了CLOSE-WAIT（关闭等待）状态。TCP服务器通知高层的应用进程，客户端向服务器的方向就释放了，这时候处于半关闭状态，即客户端已经没有数据要发送了，但是服务器若发送数据，客户端依然要接受。这个状态还要持续一段时间，也就是整个CLOSE-WAIT状态持续的时间。 客户端收到服务器的确认请求后，此时，客户端就进入FIN-WAIT-2（终止等待2）状态，等待服务器发送连接释放报文（在这之前还需要接受服务器发送的最后的数据）。 第三次挥手 服务端发送一个FIN(结束)到客户端，服务端关闭客户端的连接。 服务器将最后的数据发送完毕后，就向客户端发送连接释放报文，FIN=1，ack=u+1，由于在半关闭状态，服务器很可能又发送了一些数据，假定此时的序列号为seq=w，此时，服务器就进入了LAST-ACK（最后确认）状态，等待客户端的确认。 第四次挥手 客户端发送ACK(确认)报文确认，并将确认的序号+1，这样关闭完成。 客户端收到服务器的连接释放报文后，必须发出确认，ACK=1，ack=w+1，而自己的序列号是seq=u+1，此时，客户端就进入了TIME-WAIT（时间等待）状态。注意此时TCP连接还没有释放，必须经过2∗∗MSL（最长报文段寿命）的时间后，当客户端撤销相应的TCB后，才进入CLOSED状态。 服务器只要收到了客户端发出的确认，立即进入CLOSED状态。同样，撤销TCB后，就结束了这次的TCP连接。可以看到，服务器结束TCP连接的时间要比客户端早一些。 思考：那么为什么是4次挥手呢？ 为了确保数据能够完成传输。 关闭连接时，当收到对方的FIN报文通知时，它仅仅表示对方没有数据发送给你了；但未必你所有的数据都全部发送给对方了，所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后，再发送FIN报文给对方来表示你同意现在可以关闭连接了，所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。可能有人会有疑问，tcp我握手的时候为何ACK(确认)和SYN(建立连接)是一起发送。挥手的时候为什么是分开的时候发送呢.因为当Server端收到Client端的SYN连接请求报文后，可以直接发送SYN+ACK报文。其中ACK报文是用来应答的，SYN报文是用来同步的。但是关闭连接时，当Server端收到FIN报文时，很可能并不会立即关闭 SOCKET，所以只能先回复一个ACK报文，告诉Client端，\u0026#34;你发的FIN报文我收到了\u0026#34;。只有等到我Server端所有的报文都发送完了，我才能发送FIN报文，因此不能一起发送。故需要四步挥手。 思考:客户端突然挂掉了怎么办？ 正常连接时，客户端突然挂掉了，如果没有措施处理这种情况，那么就会出现客户端和服务器端出现长时期的空闲。解决办法是在服务器端设置保活计时器，每当服务器收到客户端的消息，就将计时器复位。超时时间通常设置为2小时。若服务器超过2小时没收到客户的信息，他就发送探测报文段。若发送了10个探测报文段，每一个相隔75秒，还没有响应就认为客户端出了故障，因而终止该连接。 23.cookie和session的区别，分布式环境怎么保存用户状态； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 cookie和session的区别，分布式环境怎么保存用户状态 1、cookie数据存放在客户的浏览器上，session数据放在服务器上。 2、cookie不是很安全，别人可以分析存放在本地的COOKIE并进行COOKIE欺骗，考虑到安全应当使用session。 3、session会在一定时间内保存在服务器上。当访问增多，会比较占用你服务器的性能，考虑到减轻服务器性能方面，应当使用COOKIE。 4、单个cookie保存的数据不能超过4K，很多浏览器都限制一个站点最多保存20个cookie。 分布式环境下的session（举例两种）： 服务器session复制 原理：任何一个服务器上的session发生改变（增删改），该节点会把这个 session的所有内容序列化，然后广播给所有其它节点，不管其他服务器需不需要session，以此来保证Session同步。 优点：可容错，各个服务器间session能够实时响应。 缺点：会对网络负荷造成一定压力，如果session量大的话可能会造成网络堵塞，拖慢服务器性能。 session共享机制 使用分布式缓存方案比如memcached、redis，但是要求Memcached或Redis必须是集群。 25.请写一段栈溢出、堆溢出的代码； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 栈溢出(StackOverflowError) 堆溢出(OutOfMemoryError:Java heap space) 永久代溢出(OutOfMemoryError: PermGen space) 直接内存溢出 /** * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public static void main(String[] args) { List\u0026lt;byte[]\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); int i=0; while(true){ list.add(new byte[5*1024*1024]); System.out.println(\u0026#34;分配次数：\u0026#34;+(++i)); } } public class StackSOFTest { int depth = 0; public void sofMethod(){ depth ++ ; sofMethod(); } public static void main(String[] args) { StackSOFTest test = null; try { test = new StackSOFTest(); test.sofMethod(); } finally { System.out.println(\u0026#34;递归次数：\u0026#34;+test.depth); } } } 26.ThreadLocal可以用来共享数据吗 1 2 ThreadLocal 解决多线程变量共享问题 ThreadLocal 不是一个线程，而是一个线程的本地化对象。当某个变量在使用 ThreadLocal 进行维护时，ThreadLocal 为使用该变量的每个线程分配了一个独立的变量副本，每个线程可以自行操作自己对应的变量副本，而不会影响其他线程的变量副本。 二.IO: 1.bio，nio，aio的区别； 1 2 3 4 5 6 7 8 9 10 11 12 13 首页 问题 匿名 BIO、NIO和AIO的区别 Java BIO： 同步并阻塞，服务器实现模式为一个连接一个线程，即客户端有连接请求时服务器端就需要启动一个线程进行处理，如果这个连接不做任何事情会造成不必要的线程开销，当然可以通过线程池机制改善。 Java NIO： 同步非阻塞，服务器实现模式为一个请求一个线程，即客户端发送的连接请求都会注册到多路复用器上，多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 Java AIO： 异步非阻塞，服务器实现模式为一个有效请求一个线程，客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。 NIO比BIO的改善之处是把一些无效的连接挡在了启动线程之前，减少了这部分资源的浪费（因为我们都知道每创建一个线程，就要为这个线程分配一定的内存空间） AIO比NIO的进一步改善之处是将一些暂时可能无效的请求挡在了启动线程之前，比如在NIO的处理方式中，当一个请求来的话，开启线程进行处理，但这个请求所需要的资源还没有就绪，此时必须等待后端的应用资源，这时线程就被阻塞了。 适用场景分析： BIO方式适用于连接数目比较小且固定的架构，这种方式对服务器资源要求比较高，并发局限于应用中，JDK1.4以前的唯一选择，但程序直观简单易理解，如之前在Apache中使用。 NIO方式适用于连接数目多且连接比较短（轻操作）的架构，比如聊天服务器，并发局限于应用中，编程比较复杂，JDK1.4开始支持，如在 Nginx，Netty中使用。（dubbo） AIO方式使用于连接数目多且连接比较长（重操作）的架构，比如相册服务器，充分调用OS参与并发操作，编程比较复杂，JDK7开始支持，在成长中，Netty曾经使用过，后来放弃。 2.nio框架：dubbo的实现原理； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 dubbo工作原理 第一层：service层，接口层，给服务提供者和消费者来实现的 第二层：config层，配置层，主要是对dubbo进行各种配置的 第三层：proxy层，服务代理层，透明生成客户端的stub和服务单的skeleton 第四层：registry层，服务注册层，负责服务的注册与发现 第五层：cluster层，集群层，封装多个服务提供者的路由以及负载均衡，将多个实例组合成一个服务 第六层：monitor层，监控层，对rpc接口的调用次数和调用时间进行监控 第七层：protocol层，远程调用层，封装rpc调用 第八层：exchange层，信息交换层，封装请求响应模式，同步转异步 第九层：transport层，网络传输层，抽象mina和netty为统一接口 第十层：serialize层，数据序列化层 工作流程： 1）第一步，provider向注册中心去注册 2）第二步，consumer从注册中心订阅服务，注册中心会通知consumer注册好的服务 3）第三步，consumer调用provider 4）第四步，consumer和provider都异步的通知监控中心 三.算法： 1.java中常说的堆和栈，分别是什么数据结构；另外，为什么要分为堆和栈来存储数据 1 2 3 堆和栈，分别是什么数据结构 栈是一种具有后进先出性质的数据结构 堆是一种经过排序的树形数据结构，每个结点都有一个值。通常我们所说的堆的数据结构，是指二叉堆 2.TreeMap如何插入数据：二叉树的左旋，右旋，双旋； 1 2 3 4 5 6 7 TreeMap的实现是红黑树算法的实现，红黑树，是一个自平衡的二叉排序树。（变色/旋转） 特性： 每个节点要么为红色，要么为黑色 根节点为黑色 每个叶子节点（NIL）是黑色 [注意：这里叶子节点，是指为空(NIL或NULL)的叶子节点！] 不允许连续两个红色节点 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑色节点 3.一个排序之后的数组，插入数据，可以使用什么方法？答：二分法；问：时间复杂度是多少？ 1 log(N) 4.平衡二叉树的时间复杂度； 1 2 O(logn) https://www.cnblogs.com/AndyAo/p/8191883.html 四. 多线程相关： 1.说说阻塞队列的实现：可以参考ArrayBlockingQueue的底层实现（锁和同步都行）； 1 2 3 4 LinkedBlockingQueue是一个基于链表实现的可选容量的阻塞队列。队头的元素是插入时间最长的，队尾的元素是最新插入的。新的元素将会被插入到队列的尾部。 LinkedBlockingQueue的容量限制是可选的，如果在初始化时没有指定容量，那么默认使用int的最大值作为队列容量。 原理 LinkedBlockingQueue中维持两把锁，一把锁用于入队，一把锁用于出队，这也就意味着，同一时刻，只能有一个线程执行入队，其余执行入队的线程将会被阻塞；同时，可以有另一个线程执行出队，其余执行出队的线程将会被阻塞。换句话说，虽然入队和出队两个操作同时均只能有一个线程操作，但是可以一个入队线程和一个出队线程共同执行，也就意味着可能同时有两个线程在操作队列，那么为了维持线程安全，LinkedBlockingQueue使用一个AtomicInterger类型的变量表示当前队列中含有的元素个数，所以可以确保两个线程之间操作底层队列是线程安全的。 2.进程通讯的方式：消息队列，共享内存，信号量，socket通讯等； 1 2 3 4 5 6 7 8 常见的通信方式 管道pipe：管道是一种半双工的通信方式，数据只能单向流动，而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 命名管道FIFO：有名管道也是半双工的通信方式，但是它允许无亲缘关系进程间的通信。 消息队列MessageQueue：消息队列是由消息的链表，存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 共享存储SharedMemory：共享内存就是映射一段能被其他进程所访问的内存，这段共享内存由一个进程创建，但多个进程都可以访问。共享内存是最快的 IPC 方式，它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制，如信号量，配合使用，来实现进程间的同步和通信。 信号量Semaphore：信号量是一个计数器，可以用来控制多个进程对共享资源的访问。它常作为一种锁机制，防止某进程正在访问共享资源时，其他进程也访问该资源。因此，主要作为进程间以及同一进程内不同线程之间的同步手段。 套接字Socket：套解口也是一种进程间通信机制，与其他通信机制不同的是，它可用于不同及其间的进程通信。 信号 ( sinal ) ： 信号是一种比较复杂的通信方式，用于通知接收进程某个事件已经发生。 5.Excutors可以产生哪些线程池； 1 2 3 4 5 Excutors 可以产生哪些线程池? 1、newCachedThreadPool：用来创建一个可缓存线程池，该线程池没有长度限制，对于新的任务，如果 有空闲的线程，则使用空闲的线程执行，如果没有，则新建一个线程来执行任务。如果线程池长度超过处理需要，可灵活回收空闲线程 2、newFixedThreadPool ：用来创建一个定长线程池，可控制线程最大并发数，超出的线程会在队列中 等待。定长线程池的大小通常根据系统资源进行设置： Runtime.getRuntime().availableProcessors() 3、newScheduledThreadPool：用来创建一个定长线程池，并且支持定时和周期性的执行任务 4、newSingleThreadExecutor：用来创建一个单线程化的线程池，它只用唯一的工作线程来执行任务， 一次只支持一个，所有任务按照指定的顺序执行 6.为什么要用线程池； 1 2 3 4 5 6 7 8 9 为什么要用线程池 当我们去创建每个线程的时候，都需要为它去分配内存，如虚拟机栈，程序计数器，本地方法栈等，所以创建的过程时间消耗是比较大的 当线程使用结束，如run方法执行结束，结束的时候，又要进行垃圾回收，又是大的时间消耗 当再执行到另一个单元的时候，又需要多个线程的时候，又重新经历从创建线程，到使用完销毁线程的过程，重复创建和销毁的这两个过程导致效率上的消耗 所以，我们思考有没有一种办法使得线程可以复用，就是执行完一个任务，并不被销毁，而是可以继续执行其他的任务，所以就诞生了线程池 降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。 提高响应速度。当任务到达时，任务可以不需要等到线程创建就能立即执行。 提高线程的可管理性。线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配、调优和监控 8.线程池原理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 线程池的两个核心队列： 线程等待池，即线程队列BlockingQueue。 任务处理池（PoolWorker），即正在工作的Thread列表（HashSet\u0026lt;Worker\u0026gt;）。 corePoolSize\t核心线程数量，线程池维护线程的最少数量 1.如果此时线程池中的数量小于corePoolSize，即使线程池中的线程都处于空闲状态，也要创建新的线程来处理被添加的任务。 2.如果此时线程池中的数量等于corePoolSize，但是缓冲队列workQueue未满，那么任务被放入缓冲队列。 3.如果此时线程池中的数量大于等于corePoolSize，缓冲队列workQueue满，并且线程池中的数量小于maximumPoolSize，建新的线程来处理被添加的任务。 4.如果此时线程池中的数量大于corePoolSize，缓冲队列workQueue满，并且线程池中的数量等于maximumPoolSize，那么通过 handler所指定的策略来处理此任务。 5.当线程池中的线程数量大于 corePoolSize时，如果某线程空闲时间超过keepAliveTime，线程将被终止。这样，线程池可以动态的调整池中的线程数。 线程池需要管理线程的生命周期，需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用，这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。 Worker是通过继承AQS，使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock，而是使用AQS，为的就是实现不可重入的特性去反应线程现在的执行状态。 使用规范： 【强制】线程资源必须通过线程池提供，不允许在应用中自行显式创建线程。 说明： 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销，解决资 源不足的问题。如果不使用线程池，有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题。 【强制】线程池不允许使用 Executors 去创建，而是通过 ThreadPoolExecutor 的方式，这样 的处理方式让写的同学更加明确线程池的运行规则，规避资源耗尽的风险。 说明： Executors 返回的线程池对象的弊端如下： 1） FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE，可能会堆积大量的请求，从而导致 OOM。 2） CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE， 可能会创建大量的线程，从而导致 OOM。 五.数据库相关（mysql） 1. 1 2 msyql优化经验 https://blog.csdn.net/qq_32332777/article/details/112617642 2.mysql的语句优化，使用什么工具； 1 2 3 4 5 6 7 8 9 10 11 Explain执行计划 1、id：SELECT识别符。这是SELECT的查询序列号； 2、select_type：查询类型，主要有PRIMARY(子查询中最外层查询）、SUBQUERY（子查询内层第一个SELECT）、UNION（UNION语句中第二个SELECT开始后面所有SELECT）、SIMPLE（除了子查询或者union之外的其他查询）； 3、table：所访问的数据库表名； 4、type：对表的访问方式，包括以下类型all(全表扫描)，index（全索引扫描），rang（索引范围扫描），ref（join语句中被驱动表索引引用查询），eq_ref（通过主键或唯一索引访问，最多只会有一条结果），const（读常量，只需读一次），system（系统表。表中只有一条数据），null（速度最快）。 5、possible_keys：查询可能使用到的索引； 6、key：最后选用的索引； 7、key_len：使用索引的最大长度； 8、ref：列出某个表的某个字段过滤； 9、rows：估算出的结果行数； 10、extra：查询细节信息，可能是以下值：distinct、using filesort（order by操作）、using index（所查数据只需要在index中即可获取）、using temporary（使用临时表）、using where（如果包含where，且不是仅通过索引即可获取内容，就会包含此信息）。 3.mysql的索引分类：B+，hash；什么情况用什么索引； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 mysql的索引分类：B+，hash；什么情况用什么索引 在MySQL的存储引擎中，MyISAM不支持哈希索引，而InnoDB中的hash索引是存储引擎根据B-Tree索引自建的 B-Tree索引的特点 1、B-tree索引可以加快数据的查询速度 存储引擎不需要进行全表扫描来获得需要的数据，取而代之的是从索引的根节点开始进行搜索。然后根据指针逐层向下查找，通过比较节点页的值和有目标值就可以找到合适的指针进入下层节点，而这些指针实际上定义了子节点页中值的上限和下限。 2、B-tree索引更适合进行范围查询 因为前面说过，B-tree对索引是顺序组织存储的，所以就很适合进行查找范围数据。 hash索引的特点 1、hash索引是基于hash表实现的，只有查询条件精确匹配hash索引中的所有列的时候，才能用到hash索引。 2、对于hash索引中的所有列，存储引擎都会为每一行计算一个hash码，hash索引中存储的就是hash码。 3、hash索引包括键值、hash码和指针 。 因为hash索引本身只需要存储对应的hash值，所以索引的结构十分紧凑，这也让hash索引查找的速度非常快。然而，hash索引也是存在其限制的： hash索引的限制 1、Hash索引必须进行二次查找 使用哈市索引两次查找，第一次找到相应的行，第二次读取数据，但是被频繁访问到的行一般会缓存在内存中，这点对数据库性能的影响不大。 2、hash索引不能用于外排序 hash索引存储的是hash码而不是键值，所以无法用于外排序 3、hash索引不支持部分索引查找也不支持范围查找 只能用到等值查询，不能范围和模糊查询 4、hash索引中的hash码的计算可能存在hash冲突 当出现hash冲突的时候，存储引擎必须遍历整个链表中的所有行指针，逐行比较，直到找到所有的符合条件的行，若hash冲突很多的话，一些索引的维护代价机会很高，所以说hash索引不适用于选择性很差的列上（重复值很多）。姓名、性别、身份证（合适） 4.mysql的存储引擎有哪些，区别是什么； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mysql的存储引擎有哪些，区别是什么 MySQL常见的三种存储引擎为InnoDB、MyISAM和MEMORY。其区别体现在事务安全、存储限制、空间使用、内存使用、插入数据的速度和对外键的支持。 1、事务安全： InnoDB支持事务安全，MyISAM和MEMORY两个不支持。 2、存储限制： InnoDB有64TB的存储限制，MyISAM和MEMORY要是具体情况而定。 3、空间使用： InnoDB对空间使用程度较高，MyISAM和MEMORY对空间使用程度较低。 4、内存使用： InnoDB和MEMORY对内存使用程度较高，MyISAM对内存使用程度较低。 5、插入数据的速度： InnoDB插入数据的速度较低，MyISAM和MEMORY插入数据的速度较高。 6、对外键的支持： InnoDB对外键支持情况较好，MyISAM和MEMORY两个不支持外键。 5.说说事务的特性和隔离级别； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 事务的特性和隔离级别 ⑴ 原子性（Atomicity） 原子性是指事务包含的所有操作要么全部成功，要么全部失败回滚，这和前面两篇博客介绍事务的功能是一样的概念，因此事务的操作如果成功就必须要完全应用到数据库，如果操作失败则不能对数据库有任何影响。 ⑵ 一致性（Consistency） 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态，也就是说一个事务执行之前和执行之后都必须处于一致性状态。 拿转账来说，假设用户A和用户B两者的钱加起来一共是5000，那么不管A和B之间如何转账，转几次账，事务结束后两个用户的钱相加起来应该还得是5000，这就是事务的一致性。 ⑶ 隔离性（Isolation） 隔离性是当多个用户并发访问数据库时，比如操作同一张表时，数据库为每一个用户开启的事务，不能被其他事务的操作所干扰，多个并发事务之间要相互隔离。 即要达到这么一种效果：对于任意两个并发的事务T1和T2，在事务T1看来，T2要么在T1开始之前就已经结束，要么在T1结束之后才开始，这样每个事务都感觉不到有其他事务在并发地执行。 关于事务的隔离性数据库提供了多种隔离级别，稍后会介绍到。 ⑷ 持久性（Durability） 持久性是指一个事务一旦被提交了，那么对数据库中的数据的改变就是永久性的，即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 四种隔离级别： ① Serializable (串行化)：可避免脏读、不可重复读、幻读的发生。 ② Repeatable read (可重复读)：可避免脏读、不可重复读的发生。 ③ Read committed (读已提交)：可避免脏读的发生。 ④ Read uncommitted (读未提交)：最低级别，任何情况都无法保证。 6.悲观锁和乐观锁的区别，怎么实现 1 2 3 4 5 6 7 8 悲观锁和乐观锁的区别，怎么实现 概念 悲观锁：一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。 乐观锁：一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。 乐观锁适用于写比较少的情况下（多读场景）。乐观锁一般会使用版本号机制或CAS算法实现。 悲观锁适用于写比较少的情况下（多写场景） 六.mq 1.mq的原理是什么： 1 2 3 4 5 6 7 MQ组成结构 Broker：消息服务器，作为server提供消息核心服务 Producer:消息生产者，业务的发起方，负责生产消息传输给broker， Consumer：消息消费者，业务的处理方，负责从broker获取消息并进行业务逻辑处理 Topic:主题，是一种消息的逻辑分类，发布订阅模式下的消息统一汇集地，不同生产者向topic发送消息，由MQ服务器分发到不同的订阅 者，实现消息的广播 Queue：队列，PTP模式下，特定生产者向特定queue发送消息，消费者订阅特定的queue完成指定消息的接收 Message：消息体，根据不同通信协议定义的固定格式进行编码的数据包，来封装业务数据，实现消息的传输 2.mq如何保证实时性； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mq如何保证实时性 1.生产者将数据发送到 RabbitMQ 的时候，此时可以选择用 RabbitMQ 提供的事务功能，就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect，然后发送消息，如果消息没有成功被 RabbitMQ 接收到，那么生产者会收到异常报错，此时就可以回滚事务channel.txRollback，然后重试发送消息；如果收到了消息，那么可以提交事务channel.txCommit。（缺点：吞吐量会下来，因为太耗性能） 一般来说，如果你要确保说写 RabbitMQ 的消息别丢，可以开启 confirm 模式，在生产者那里设置开启 confirm 模式之后，你每次写的消息都会分配一个唯一的 id，然后如果写入了 RabbitMQ 中，RabbitMQ 会给你回传一个 ack 消息，告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息，会回调你的一个 nack 接口，告诉你这个消息接收失败，你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态，如果超过一定时间还没接收到这个消息的回调，那么你可以重发。 事务机制和 cnofirm 机制最大的不同在于，事务机制是同步的，你提交一个事务之后会阻塞在那儿，但是 confirm 机制是异步的，你发送个消息之后就可以发送下一个消息，然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。 所以一般在生产者这块避免数据丢失，都是用 confirm 机制的。 2.RabbitMQ 弄丢了数据 就是 RabbitMQ 自己弄丢了数据，这个你必须开启 RabbitMQ 的持久化，就是消息写入之后会持久化到磁盘，哪怕是 RabbitMQ 自己挂了，恢复之后会自动读取之前存储的数据，一般数据不会丢。除非极其罕见的是，RabbitMQ 还没持久化，自己就挂了，可能导致少量数据丢失，但是这个概率较小。 设置持久化有两个步骤： 创建 queue 的时候将其设置为持久化 这样就可以保证 RabbitMQ 持久化 queue 的元数据，但是它是不会持久化 queue 里的数据的。 第二个是发送消息的时候将消息的 deliveryMode 设置为 2 就是将消息设置为持久化的，此时 RabbitMQ 就会将消息持久化到磁盘上去。 3.消费端弄丢了数据 RabbitMQ 如果丢失了数据，主要是因为你消费的时候，刚消费到，还没处理，结果进程挂了，比如重启了，那么就尴尬了，RabbitMQ 认为你都消费了，这数据就丢了。 这个时候得用 RabbitMQ 提供的 ack 机制，简单来说，就是你必须关闭 RabbitMQ 的自动 ack，可以通过一个 api 来调用就行，然后每次你自己代码里确保处理完的时候，再在程序里 ack 一把。这样的话，如果你还没处理完，不就没有 ack 了？那 RabbitMQ 就认为你还没处理完，这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理，消息是不会丢的。 3.mq的持久化是怎么做的； 1 2 3 4 mq的持久化是怎么做的 队列持久化需要在声明队列时添加参数 durable=True，这样在rabbitmq崩溃时也能保存队列 仅仅使用durable=True ，只能持久化队列，不能持久化消息 消息持久化需要在消息生成时，添加参数 properties=pika.BasicProperties(delivery_mode=2) 七.nosql相关（主要是redis） 1.redis和memcache的区别 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 redis和memcache的区别； 1、 Redis和Memcache都是将数据存放在内存中，都是内存数据库。不过memcache还可用于缓存其他东西，例如图片、视频等等。 2、Redis不仅仅支持简单的k/v类型的数据，同时还提供list，set，hash等数据结构的存储。 3、虚拟内存–Redis当物理内存用完时，可以将一些很久没用到的value 交换到磁盘 4、过期策略–memcache在set时就指定，例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定，例如expire name 10 5、分布式–设定memcache集群，利用magent做一主多从;redis可以做一主多从。都可以一主一从 6、存储数据安全–memcache挂掉后，数据没了；redis可以定期保存到磁盘（持久化） 7、灾难恢复–memcache挂掉后，数据不可恢复; redis数据丢失后可以通过aof恢复 8、Redis支持数据的备份，即master-slave模式的数据备份。 关于redis和memcache的不同，下面罗列了一些相关说法，供记录： redis和memecache的不同在于： 1、存储方式： memecache 把数据全部存在内存之中，断电后会挂掉，数据不能超过内存大小 redis有部份存在硬盘上，这样能保证数据的持久性，支持数据的持久化（笔者注：有快照和AOF日志两种持久化方式，在实际应用的时候，要特别注意配置文件快照参数，要不就很有可能服务器频繁满载做dump）。 2、数据支持类型： redis在数据支持上要比memecache多的多。 3、使用底层模型不同： 新版本的redis直接自己构建了VM 机制 ，因为一般的系统调用系统函数的话，会浪费一定的时间去移动和请求。 4、运行环境不同： redis目前官方只支持LINUX 上去行，从而省去了对于其它系统的支持，这样的话可以更好的把精力用于本系统 环境上的优化，虽然后来微软有一个小组为其写了补丁。但是没有放到主干上 个人总结一下，有持久化需求或者对数据结构和处理有高级要求的应用，选择redis，其他简单的key/value存储，选择memcache。 3.redis是如何持久化的：rdb和aof； 1 2 3 4 5 RDB持久化 RDB持久化是将进程数据写入文件，RDB持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化)，保存的文件后缀是rdb；当Redis重新启动时，可以读取快照文件恢复数据。 AOF持久化 AOF持久化(即Append Only File持久化)，则是将Redis执行的每次写命令记录到单独的日志文件中。 随着时间的流逝，你会发现这个AOF文件越来越大，于是redis有一套rewrite机制，来缩小AOF文件的体积。然而，在rewrite的过程中也是需要父进程来fork出一个子进程进行rewrite操作。因此AOF也是会影响redis的性能的。 4.redis集群如何同步； 1 2 3 4 5 6 7 8 9 10 1、从服务器向主服务器发送SYNC命令； 2、收到SYNC命令的主服务器执行BGSAVE命令，在后台生成一个RDB文件，并使用一个缓冲区记录从现在开始执行的所有写命令； 3、当主服务器的BGSAVE命令执行完毕时，主服务器会将BGSAVE命令生成的RDB文件发送给从服务器，从服务器接收并载入这个RDB文件，将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。 4、主服务器将记录在缓冲区里面的所有写命令发送给从服务器，从服务器执行这些写命令，将自己的数据库状态更新至主服务器数据库当前所处的状态。 SYNC命令是非常消耗资源的，因为每次执行SYNC命令，主从服务器需要执行一下操作： 1、主服务器需要执行BGSAVE命令来生成RDB文件，这个生成操作会耗费主服务器大量的CPU、内存和磁盘I/O资源； 2、主服务器需要将自己生成的RDB文件发送给从服务器，这个发送操作会耗费主从服务器大量的网络资源（带宽和流量），并对主服务器响应命令请求的时间产生影响； 3、接收到RDB文件的从服务器需要载入主服务器发来的RDB文件，并且在载入期间，从服务器会因为阻塞而没办法处理命令请求。 SYNC是一个如此消耗资源的命令，所以Redis最好在真需要的时候才需要执行SYNC命令。 5.redis的数据添加过程是怎样的：哈希槽； 1 2 哈希槽是用来决定这个key存在哪个节点，通过哈希计算，得到槽值为1，那么这个数据将存到节点1的Redis上 一个 redis 集群包含 16384 个哈希槽（hash slot），数据库中的每个数据都属于这16384个哈希槽中的一个。集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽。集群中的每一个节点负责处理一部分哈希槽。 6.redis的淘汰策略有哪些； 1 2 3 4 5 6 7 redis内存数据数据集大小升到一定大的时候，就会实行数据淘汰策略（回收策略）。 1，volatile-lru：从已设置过期时间的哈希表(server.db[i].expires)中随机挑选多个key,然后在选到的key中用lru算法淘汰最近最少使用的数据 2，allkey-lru：从所有key的哈希表（server.db[i].dict）中随机挑选多个key,然后再选到的key中利用lru算法淘汰最近最少使用的数据 3，volatile-ttl：从已设置过期时间的哈希表（server.db[i].expires)中随机挑选多个key,然后在选到的key中选择过期时间最小的数据淘汰掉。 4，volatile-random：从已设置过期时间的哈希表（server.db[i].expires）中随机挑选key淘汰掉。 5，allkey-random：从所有的key的哈希表（server.db[i].dict）中随机挑数据淘汰 6，no-eviction（驱逐）：内存达到上限，不淘汰数据 ","date":"2021-04-21T00:00:00Z","permalink":"/p/java%E9%9D%A2%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86%E6%80%BB%E7%BB%93/","title":"java面试题整理总结"}]