分布式微服务系统架构第104集:类加载的过程,高性能、高可靠性下单服务系统的核心特性

17 阅读10分钟

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc…

1024bat.cn/

类加载的过程

类加载过程包括 5 个阶段:加载、验证、准备、解析和初始化。

加载

加载的过程

“加载”是“类加载”过程的一个阶段,不能混淆这两个名词。在加载阶段,虚拟机需要完成 3 件事:

  • 通过类的全限定名获取该类的二进制字节流。
  • 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。
  • 在内存中创建一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

获取二进制字节流

对于 Class 文件,虚拟机没有指明要从哪里获取、怎样获取。除了直接从编译好的 .class 文件中读取,还有以下几种方式:

  • 从 zip 包中读取,如 jar、war 等;
  • 从网络中获取,如 Applet;
  • 通过动态代理技术生成代理类的二进制字节流;
  • 由 JSP 文件生成对应的 Class 类;
  • 从数据库中读取,如 有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

“非数组类”与“数组类”加载比较

  • 非数组类加载阶段可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器控制字节流的获取方式(如重写一个类加载器的 loadClass() 方法)。
  • 数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的,再由类加载器创建数组中的元素类。

注意事项

  • 虚拟机规范未规定 Class 对象的存储位置,对于 HotSpot 虚拟机而言,Class 对象比较特殊,它虽然是对象,但存放在方法区中。
  • 加载阶段与连接阶段的部分内容交叉进行,加载阶段尚未完成,连接阶段可能已经开始了。但这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证的重要性

验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证的过程

  • 文件格式验证 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理,验证点如下:

    • 是否以魔数 0XCAFEBABE 开头。
    • 主次版本号是否在当前虚拟机处理范围内。
    • 常量池是否有不被支持的常量类型。
    • 指向常量的索引值是否指向了不存在的常量。
    • CONSTANT_Utf8_info 型的常量是否有不符合 UTF8 编码的数据。
    • ......
  • 元数据验证 对字节码描述信息进行语义分析,确保其符合 Java 语法规范。

  • 字节码验证 本阶段是验证过程中最复杂的一个阶段,是对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。

  • 符号引用验证 本阶段发生在解析阶段,确保解析正常执行。

准备

准备阶段是正式为类变量(或称“静态成员变量”)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。

初始值“通常情况下”是数据类型的零值(0, null...),假设一个类变量的定义为:

public static int value = 123;

那么变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法。

存在“特殊情况”:如果类字段的字段属性表中存在 ConstantValue 属性,那么在准备阶段 value 就会被初始化为 ConstantValue 属性所指定的值,假设上面类变量 value 的定义变为:

public static final int value = 123;

那么在准备阶段虚拟机会根据 ConstantValue 的设置将 value 赋值为 123。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

类初始化阶段是类加载过程的最后一步,是执行类构造器 <clinit>() 方法的过程。

<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。如下方代码所示:

public class Test {
    static {
        i = 0;  // 给变量赋值可以正常编译通过
        System.out.println(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

<clinit>() 方法不需要显式调用父类构造器,虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕。

由于父类的 <clinit>() 方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下方代码所示:

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
    System.out.println(Sub.B); // 输出 2
}

<clinit>() 方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法。

接口中不能使用静态代码块,但接口也需要通过 <clinit>() 方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的 <clinit>() 方法不需要先执行父类的 <clinit>() 方法,只有当父接口中定义的变量使用时,父接口才会初始化。

虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法。

1. 布隆过滤器(Bloom Filter)

  • 作用:用于快速判断一个元素是否不存在于某个集合中,避免缓存穿透。

  • 典型场景

    • 用户请求查询一个商品,如果商品 ID 不存在于布隆过滤器中,就可以直接返回不访问数据库和缓存
    • 通常在缓存系统(如 Redis)前使用。
  • 优点:节省内存、查询速度快。

  • 缺点:有一定误判率(可能判断“存在”而实际上不存在),但不会误判为“不存在”。


2. 延迟双删(Cache Double Delete)

  • 作用:解决缓存一致性问题。

  • 操作步骤

    1. 删除缓存;
    2. 更新数据库;
    3. 延迟一段时间后再次删除缓存(防止并发读请求将旧值写回缓存)。
  • 典型应用

    • 数据更新场景中,为了防止缓存脏数据回写。
  • 增强版本:可搭配分布式锁、消息队列实现更强一致性。


3. 死信队列(DLQ - Dead Letter Queue)

  • 作用:处理无法消费或失败的消息,防止消息丢失。

  • 适用情况

    • 消费失败次数超过阈值;
    • 消息格式不合法;
    • 消费处理超时。
  • 实践建议

    • 将失败消息放入 DLQ;
    • 后续进行人工或定时任务回查;
    • 可结合告警系统及时响应。

4. Tracing + Metrics(链路追踪 + 指标监控)

  • Tracing(链路追踪)

    • 工具:Zipkin、Jaeger、SkyWalking、Sleuth。
    • 用途:追踪分布式系统中一次请求跨服务的调用链路(包括耗时、异常点等)。
  • Metrics(指标监控)

    • 工具:Prometheus + Grafana。

    • 指标类型:

      • QPS、RT、失败率、内存/线程数、Kafka lag 等;
      • 应用于服务水平指标(SLI)、告警等。

5. 消费重试机制

  • 作用:保证 Kafka/RocketMQ 等 MQ 的消息最终处理成功。

  • 方式

    • 自动重试(如 Spring Cloud Stream 的 maxAttempts);
    • 业务手动捕捉异常 + 重投
    • 异步重试队列(例如 Redis 延迟队列 / RocketMQ 延迟消息);
    • 最终落入 DLQ。
  • 注意事项

    • 幂等性是前提;
    • 防止消息积压;
    • 设置最大重试次数和重试间隔。

示例整合场景:用户下单服务

  • 布隆过滤器过滤非法商品 ID;
  • Redis 缓存商品详情,使用延迟双删保持一致;
  • Kafka 订单创建消息消费失败时重试处理;
  • 重试失败落入 DLQ;
  • 使用 Tracing 追踪整个下单过程;
  • Prometheus 收集订单创建成功率、失败率等指标。

项目结构概览(简化)

arduino
复制编辑
order-service/
├── controller/
│   └── OrderController.java
├── service/
│   ├── OrderService.java
│   └── OrderConsumer.java
├── config/
│   ├── RedisConfig.java
│   ├── KafkaConfig.java
│   └── TracingConfig.java
├── bloom/
│   └── BloomFilterService.java
├── retry/
│   └── KafkaRetryHandler.java
├── metrics/
│   └── OrderMetricsCollector.java

🧩 核心代码片段(逐模块拆解)

1️⃣ 布隆过滤器过滤非法商品 ID
@Component
public class BloomFilterService {
    private BloomFilter<String> bloomFilter;

    @PostConstruct
    public void init() {
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 1000000, 0.01);
        // 假设从 DB 加载商品 ID
        List<String> allProductIds = productRepository.getAllProductIds();
        allProductIds.forEach(bloomFilter::put);
    }

    public boolean mightContain(String productId) {
        return bloomFilter.mightContain(productId);
    }
}
2️⃣ Redis 缓存 + 延迟双删
@Service
public class ProductCacheService {

    @Autowired
    private RedisTemplate<String, Product> redisTemplate;

    private static final String KEY_PREFIX = "product:";

    public Product getProduct(String productId) {
        return redisTemplate.opsForValue().get(KEY_PREFIX + productId);
    }

    public void deleteProductCache(String productId) {
        redisTemplate.delete(KEY_PREFIX + productId);
    }

    public void updateProduct(Product product) {
        deleteProductCache(product.getId());
        productRepository.save(product);
        // 延迟双删(异步)
        Executors.newSingleThreadScheduledExecutor().schedule(
            () -> deleteProductCache(product.getId()), 2, TimeUnit.SECONDS
        );
    }
}
3️⃣ Kafka 下单消息 + 重试 + DLQ
@KafkaListener(topics = "order-topic", groupId = "order-group")
public void handleOrderMessage(String message) {
    try {
        orderService.process(message);
    } catch (Exception e) {
        // 自定义重试机制 or 抛出异常让 Kafka 自动重试
        retryHandler.retry(message, e);
    }
}

重试处理示例:

@Component
public class KafkaRetryHandler {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void retry(String msg, Exception e) {
        int retries = extractRetryCount(msg);
        if (retries < 3) {
            kafkaTemplate.send("order-retry-topic", incrementRetryHeader(msg));
        } else {
            kafkaTemplate.send("order-dlq", msg); // 死信队列
        }
    }
}
4️⃣ Tracing(Sleuth + Zipkin)
@RestController
public class OrderController {

    @Autowired
    private Tracer tracer;

    @Autowired
    private OrderService orderService;

    @PostMapping("/order")
    public ResponseEntity<String> placeOrder(@RequestBody OrderRequest request) {
        Span newSpan = tracer.nextSpan().name("order_request").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(newSpan.start())) {
            orderService.placeOrder(request);
            return ResponseEntity.ok("Order placed.");
        } finally {
            newSpan.end();
        }
    }
}

Spring Cloud Sleuth 会自动传递 traceId 到 Kafka 消费者。

5️⃣ Prometheus Metrics(Micrometer)
@Component
public class OrderMetricsCollector {

    private Counter successCounter;
    private Counter failureCounter;

    public OrderMetricsCollector(MeterRegistry registry) {
        successCounter = Counter.builder("order_success_total").register(registry);
        failureCounter = Counter.builder("order_failure_total").register(registry);
    }

    public void incrementSuccess() {
        successCounter.increment();
    }

    public void incrementFailure() {
        failureCounter.increment();
    }
}

✅ 总体架构图(简化)

用户请求
   |
   v
[布隆过滤器] ---> [查询缓存] --> 缓存命中 --> 返回
   |                     |
   |                     v
   |                [加分布式锁]
   v                     |
请求通过 --> [查数据库 + 延迟双删 Redis]
   |
   v
[Kafka 异步下单消息投递]  -----> 消费失败 --> [Retry Topic / DLQ]
   |
   v
[链路追踪 Tracing + Metrics 指标上报]

☁️ 1. 布隆过滤器(Bloom Filter)

用于防止缓存穿透,通常初始化时将所有商品 ID 加入布隆过滤器。

初始化布隆过滤器(使用 Guava 或 RedisBloom)

BloomFilter<Long> bloomFilter = BloomFilter.create(
    Funnels.longFunnel(), 500_000, 0.01
);

// 加载商品 ID
for (Long skuId : skuService.getAllSkuIds()) {
    bloomFilter.put(skuId);
}

// 判断
if (!bloomFilter.mightContain(skuId)) {
    throw new RuntimeException("商品不存在");
}

🧠 2. 缓存延迟双删(防止并发缓存穿透)

public OrderDTO getOrder(Long orderId) {
    String key = "order:" + orderId;
    OrderDTO order = redisTemplate.opsForValue().get(key);
    if (order != null) return order;

    // 加锁防击穿
    RLock lock = redissonClient.getLock("lock:order:" + orderId);
    lock.lock();
    try {
        // 查询数据库
        order = orderMapper.selectById(orderId);
        redisTemplate.opsForValue().set(key, order, 30, TimeUnit.MINUTES);

        // 延迟双删线程(异步执行)
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000); // 延迟1秒
                redisTemplate.delete(key);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        return order;
    } finally {
        lock.unlock();
    }
}

💥 3. Kafka 死信队列(DLQ) + 消费重试机制

Kafka Producer 发送下单消息:

OrderMessage msg = new OrderMessage(orderId, userId);
kafkaTemplate.send("order_topic", JSON.toJSONString(msg));

Kafka Listener(带重试和 DLQ)

@KafkaListener(topics = "order_topic", groupId = "order_group")
public void consume(String message, Acknowledgment ack) {
    try {
        OrderMessage orderMsg = JSON.parseObject(message, OrderMessage.class);
        orderService.createOrder(orderMsg);
        ack.acknowledge();
    } catch (Exception e) {
        log.error("消费失败:{}", message, e);
        // 失败不手动 ack,自动转发到 Retry 或 DLQ
    }
}

Kafka 配置(application.yml)

spring:
  kafka:
    listener:
      ack-mode: manual
      retry:
        enabled: true
        max-attempts: 3
        backoff:
          delay: 1000
          multiplier: 2
    consumer:
      enable-auto-commit: false
    producer:
      retries: 3
    topics:
      retry: order_topic_retry
      dlq: order_topic_dlq

📊 4. Tracing(链路追踪)+ Metrics(指标监控)

Tracing:用 Spring Cloud Sleuth + Zipkin/Jaeger

<!-- 添加 Sleuth 和 Zipkin 依赖 -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

自动生成 TraceId/SpanId,Zipkin UI 可查看服务调用链。


Metrics:使用 Micrometer + Prometheus + Grafana

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

暴露 /actuator/prometheus 端点,在代码中埋点:

@Autowired
private MeterRegistry meterRegistry;

public void createOrder(OrderDTO dto) {
    Timer.Sample sample = Timer.start(meterRegistry);
    try {
        // 核心逻辑
    } finally {
        sample.stop(meterRegistry.timer("order.create.timer"));
    }
}

🔁 5. 消费重试机制方案

✅ 自动重试(配合 Spring Kafka)

  • 配置 max-attempts
  • 设置 exponential backoff
  • 超过最大重试 --> 发送到 DLQ 由人工或告警处理

✅ 手动重试(持久化到 Redis/DB)

// 消费失败写入 retry-task 表,后台定时任务重新推送 Kafka