design\project\Vertx(五)

290 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天

Vertx 官方 DEMO 示栗(三)

Vertx 快速上手指北(三)

第三章: Vertx 中的:EventBus(事件总线)

本章大纲:

  1. 事件总线 EventBus 是什么?
  2. 能干什么?
  3. 怎么用?
  4. 有哪些消息传递模式?
  5. 事件总线 EventBus 的集群特性?
  6. 使用事件总线 EventBus 发布服务接口(请听下下章分解)

个人总结:

  1. 本章概要:Vertx “事件总线(EventBus)” 的基础用法
  2. 本章是非常 “干货” ,直接上了一个 “栗子
  3. 上完 “栗子” 直接上 “集群栗子” 🥱 呃……
  4. 等吃饱了,我们再具体说说 EventBus 的消息传递模型

废话不说,直接上栗子

博主亲自上手画的栗子图
graph TD

温度传感器HeatSensor1 --> 传感器问题日志记录程序
温度传感器HeatSensor2 --> 传感器问题日志记录程序
温度传感器HeatSensor... --> 传感器问题日志记录程序
温度传感器HeatSensorN --> 传感器问题日志记录程序
温度传感器HeatSensor1 --> HTTP服务器
温度传感器HeatSensor2 --> HTTP服务器
温度传感器HeatSensor... --> HTTP服务器
温度传感器HeatSensorN --> HTTP服务器
HTTP服务器 --最新数据--> 传感器数据服务SensorData
传感器数据服务SensorData --平均值--> HTTP服务器
HTTP服务器 ---> WEB客户端
HTTP服务器 ---> SSE长连接客户端

图:栗子执行逻辑

  • HeatSensor
/**
 * 以非固定速率生成温度测量值
 *    并将其发布给传感器的订阅者
 *    每个 HeatSensor 都有一个唯一的传感器标识符。
 */
public class HeatSensor extends AbstractVerticle {

  private final Random random = new Random();

  /**
   * 温度传感器的唯一 ID
   */
  private final String sensorId = UUID.randomUUID().toString();
  private double temperature = 21.0;

  @Override
  public void start() {
    scheduleNextUpdate();
  }

  private void scheduleNextUpdate() {
    vertx.setTimer(random.nextInt(5000) + 1000, this::update);
  }

  /**
   * 获得平均温度,并发布最新温度事件,publish 模式
   * @param timerId
   */
  private void update(long timerId) {
    temperature = temperature + (delta() / 10);
    JsonObject payload = new JsonObject()
      .put("id", sensorId)
      .put("temp", temperature);
    vertx.eventBus().publish("sensor.updates", payload);
    scheduleNextUpdate();
  }

  private double delta() {
    if (random.nextInt() > 0) {
      return random.nextGaussian();
    } else {
      return -random.nextGaussian();
    }
  }
}
  • 传感器问题日志记录程序
/**
 * 监视新的温度测量值
 * 并使用SLF4J记录它们。
 */
public class Listener extends AbstractVerticle {

  private final Logger logger = LoggerFactory.getLogger(Listener.class);
  private final DecimalFormat format = new DecimalFormat("#.##");

  /**
   * 记录日志
   */
  @Override
  public void start() {
    EventBus bus = vertx.eventBus();
    bus.<JsonObject>consumer("sensor.updates", msg -> {
      JsonObject body = msg.body();
      String id = body.getString("id");
      String temperature = format.format(body.getDouble("temp"));
      logger.info("{} reports a temperature ~{}C", id, temperature);
    });
  }
}
  • HTTP服务器
/**
 * HttpServer 创建 HTTP 服务器
 *    为 web 界面提供服务
 *    每当观察到一个新的温度测量时,它就会向它的客户端推送新值(SSE)
 *    并定期请求当前平均值并更新所有客户端。
 */
public class HttpServer extends AbstractVerticle {

  /**
   * 创建 HTTP 服务器
   */
  @Override
  public void start() {
    vertx.createHttpServer()
      .requestHandler(this::handler)
      .listen(config().getInteger("port", 8080));
  }

  /**
   * 响应数据
   * @param request
   */
  private void handler(HttpServerRequest request) {
    if ("/".equals(request.path())) {
      request.response().sendFile("index.html");
    } else if ("/sse".equals(request.path())) {
      sse(request);
    } else {
      request.response().setStatusCode(404);
    }
  }

  /**
   * SSE 长连接示栗
   * @param request
   */
  private void sse(HttpServerRequest request) {
    HttpServerResponse response = request.response();
    response
      // 标记 MIME 为 SSE
      .putHeader("Content-Type", "text/event-stream")
      // 实时流,禁用浏览器缓存
      .putHeader("Cache-Control", "no-cache")
      //
      .setChunked(true);

    /**
     * 每个 sse 长连接启动一个订阅,用于实时推送结果数据
     */
    MessageConsumer<JsonObject> consumer = vertx.eventBus().consumer("sensor.updates");
    consumer.handler(msg -> {
      response.write("event: update\n");
      response.write("data: " + msg.body().encode() + "\n\n");
    });


    /**
     * 启动一个 request 获得数据
     *    periodicStream 类似 AIO,有比 setPeriodic 更多的 api
     *      支持主动 fetch 模式 pause
     *      支持通过 pipe() 将 periodic 中的结果转发给另外的 WriteStream
     *      相当于一个 Stream 包装的 setPeriodic
     */
    TimeoutStream ticks = vertx.periodicStream(1000);
    ticks.handler(id -> {
      vertx.eventBus().<JsonObject>request("sensor.average", "", reply -> {
        if (reply.succeeded()) {
          response.write("event: average\n");
          response.write("data: " + reply.result().body().encode() + "\n\n");
        }
      });
    });

    // 提供了一种异步接收 response 的思路
    response.endHandler(v -> {
      consumer.unregister();
      ticks.cancel();
    });
  }
}
  • 传感器数据服务SensorData
/**
 * SensorData
 *    保存每个传感器的最新观测值的记录。
 *    它还支持基于 reply 模式的平均值计算: 回复基于最新数据的平均值 average
 */
public class SensorData extends AbstractVerticle {

  /**
   * 保存每个传感器的最新观测值的记录
   */
  private final HashMap<String, Double> lastValues = new HashMap<>();

  /**
   * 订阅最新的温度记录
   * 订阅温度平均值计算
   */
  @Override
  public void start() {
    EventBus bus = vertx.eventBus();
    bus.consumer("sensor.updates", this::update);
    bus.consumer("sensor.average", this::average);
  }

  private void update(Message<JsonObject> message) {
    JsonObject json = message.body();
    lastValues.put(json.getString("id"), json.getDouble("temp"));
  }

  private void average(Message<JsonObject> message) {
    double avg = lastValues.values().stream()
      .collect(Collectors.averagingDouble(Double::doubleValue));
    JsonObject json = new JsonObject().put("average", avg);
    message.reply(json);
  }
}
单体部署
public class Main {

  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle("chapter3.HeatSensor", new DeploymentOptions().setInstances(4));
    vertx.deployVerticle("chapter3.Listener");
    vertx.deployVerticle("chapter3.SensorData");
    vertx.deployVerticle("chapter3.HttpServer");
  }
}
集群栗子
  • 云端集群部署
public class FirstInstance {

  private static final Logger logger = LoggerFactory.getLogger(FirstInstance.class);

  /**
   * vertx 集群(cluster)模式实例
   * @param args
   */
  public static void main(String[] args) {
    Vertx.clusteredVertx(new VertxOptions(), ar -> {
      if (ar.succeeded()) {
        logger.info("First instance has been started");
        Vertx vertx = ar.result();
        // 可以直接使用类型全限定名来启动实例
        vertx.deployVerticle("chapter3.HeatSensor", new DeploymentOptions().setInstances(4));
        vertx.deployVerticle("chapter3.HttpServer");
      } else {
        logger.error("Could not start", ar.cause());
      }
    });
  }
}
  • 局域网端集群部署
public class SecondInstance {

  private static final Logger logger = LoggerFactory.getLogger(SecondInstance.class);

  public static void main(String[] args) {
    Vertx.clusteredVertx(new VertxOptions(), ar -> {
      if (ar.succeeded()) {
        logger.info("Second instance has been started");
        Vertx vertx = ar.result();
        vertx.deployVerticle("chapter3.HeatSensor", new DeploymentOptions().setInstances(4));
        vertx.deployVerticle("chapter3.Listener");
        vertx.deployVerticle("chapter3.SensorData");
        JsonObject conf = new JsonObject().put("port", 8081);
        vertx.deployVerticle("chapter3.HttpServer", new DeploymentOptions().setConfig(conf));
      } else {
        logger.error("Could not start", ar.cause());
      }
    });
  }
}

栗子吃好了,我们来唠唠

  • 什么是事件总线

    事件总线是一种以异步方式发送和接收消息的方法。向目的地发送消息,并从目的地检索消息。含

    • body
      • 通常使用 Vert.x JSON 格式进行编码
      • 提供多语言支持
      • 可以注册自定义编码器/解码器(codecs)
        • 博主温馨提示:截至目前,支持力度一般般,可用但不是很灵活(效率至上)
      • 支持通过自定义 codecs 直接传输 Java 对象
    • 存储元数据的可选 headers
    • 过期时间戳 expiration
      • 过期未处理的消息将被丢弃
  • 事件总线可以扩展到应用程序进程之外

    • 事件总线也可以跨集群
    • 事件总线可以直连到 嵌入式
    • 事件总线可以直连到 外部消息代理
    • 事件总线可以直连到 远程客户端
    • 事件总线可以直连到 web浏览器中的 js 应用程序

image.png

图:除了官方提供的,还有这么多客户端们

消息传递模式
  • 点到点消息传递

2.1.png

  • 请求-应答消息

2.2.png

  • 发布/订阅消息传递

2.3.png

  • 感觉不是很好使?请听下回再分解
你这个事件总线,他能当 MQ 用么 🤔?

简而言之 事件总线 是 “事件” 总线, MQ 是消息 MQ

  • 和 MQ 相似之处:常见的消息传递模式

    • 例如发布/订阅模式,该模式在集成分布式和异构应用程序时非常流行
  • 不同之处:

    • 用于应用程序内部的 Verticle 间的通信,而不是消息用于应用程序之间的通信总线
  • 不同于 ActiveMQ、RabbitMQ、ZeroMQ或Apache Kafka

    • Vertx 可以与这些消息 Broker 继承,事件总线不能替代它们
    • eventBus 消息不能 ack 确认
    • eventBus 消息无优先级
    • eventBus 消息不能做路由规则
    • eventBus 消息不能做转换规则(模式适应,散/聚,等等)
    • eventBus 消息不能从崩溃中恢复(持久化)
  • 事件总线仅仅携带用于 Verticle 服务做异步处理的原子(volatile)事件。

你说你这个 EventBus 不能从崩溃中回复,那你这个消息,它丢了咋办
  • 官方说的:同时使用中间件(成本更高)来防止事件丢失,请听下回分解。所以,给点时间,博主找找……………………………………😶(PS 博主已经找了好几章了,还是没有………………………………)
集群模式下的 EventBus

启用分布式事件总线对于服务是无感的。

事件总线API具有用于声明消息处理程序的localConsumer方法,当与集群一起运行时,这些消息处理程序仅在本地工作。

本章完……………