【问题标题】:Send data in multiple ways depending on how you want to send it根据您的发送方式以多种方式发送数据
【发布时间】:2018-01-11 07:35:03
【问题描述】:

我有一堆键和值,我想通过将它们打包在一个字节数组中来发送到我们的消息队列。我将创建一个字节数组,包含所有应该始终小于 50K 的键和值,然后发送到我们的消息队列。

包类

public final class Packet implements Closeable {
  private static final int MAX_SIZE = 50000;
  private static final int HEADER_SIZE = 36;

  private final byte dataCenter;
  private final byte recordVersion;
  private final long address;
  private final long addressFrom;
  private final long addressOrigin;
  private final byte recordsPartition;
  private final byte replicated;
  private final ByteBuffer itemBuffer = ByteBuffer.allocate(MAX_SIZE);
  private int pendingItems = 0;

  public Packet(final RecordPartition recordPartition) {
    this.recordsPartition = (byte) recordPartition.getPartition();
    this.dataCenter = Utils.LOCATION.get().datacenter();
    this.recordVersion = 1;
    this.replicated = 0;
    final long packedAddress = new Data().packAddress();
    this.address = packedAddress;
    this.addressFrom = 0L;
    this.addressOrigin = packedAddress;
  }

  private void addHeader(final ByteBuffer buffer, final int items) {
    buffer.put(dataCenter).put(recordVersion).putInt(items).putInt(buffer.capacity())
        .putLong(address).putLong(addressFrom).putLong(addressOrigin).put(recordsPartition)
        .put(replicated);
  }

  private void sendData() {
    if (itemBuffer.position() == 0) {
      // no data to be sent
      return;
    }
    final ByteBuffer buffer = ByteBuffer.allocate(MAX_SIZE);
    addHeader(buffer, pendingItems);
    buffer.put(itemBuffer);
    SendRecord.getInstance().sendToQueueAsync(address, buffer.array());
    // SendRecord.getInstance().sendToQueueAsync(address, buffer.array());
    // SendRecord.getInstance().sendToQueueSync(address, buffer.array());
    // SendRecord.getInstance().sendToQueueSync(address, buffer.array(), socket);
    itemBuffer.clear();
    pendingItems = 0;
  }

  public void addAndSendJunked(final byte[] key, final byte[] data) {
    if (key.length > 255) {
      return;
    }
    final byte keyLength = (byte) key.length;
    final byte dataLength = (byte) data.length;

    final int additionalSize = dataLength + keyLength + 1 + 1 + 8 + 2;
    final int newSize = itemBuffer.position() + additionalSize;
    if (newSize >= (MAX_SIZE - HEADER_SIZE)) {
      sendData();
    }
    if (additionalSize > (MAX_SIZE - HEADER_SIZE)) {
      throw new AppConfigurationException("Size of single item exceeds maximum size");
    }

    final ByteBuffer dataBuffer = ByteBuffer.wrap(data);
    final long timestamp = dataLength > 10 ? dataBuffer.getLong(2) : System.currentTimeMillis();
    // data layout
    itemBuffer.put((byte) 0).put(keyLength).put(key).putLong(timestamp).putShort(dataLength)
        .put(data);
    pendingItems++;
  }

  @Override
  public void close() {
    if (pendingItems > 0) {
      sendData();
    }
  }
}

以下是我发送数据的方式。截至目前,我的设计只允许通过在上述sendData() 方法中调用sendToQueueAsync 方法来异步发送数据。

  private void validateAndSend(final RecordPartition partition) {
    final ConcurrentLinkedQueue<DataHolder> dataHolders = dataHoldersByPartition.get(partition);

    final Packet packet = new Packet(partition);

    DataHolder dataHolder;
    while ((dataHolder = dataHolders.poll()) != null) {
      packet.addAndSendJunked(dataHolder.getClientKey().getBytes(StandardCharsets.UTF_8),
          dataHolder.getProcessBytes());
    }
    packet.close();
  }

现在我需要扩展我的设计,以便我可以通过三种不同的方式发送数据。由用户决定他想以哪种方式发送数据,“同步”或“异步”。

  • 我需要通过调用sender.sendToQueueAsync方法异步发送数据。
  • 或者我需要调用sender.sendToQueueSync方法同步发送数据。
  • 或者我需要通过调用sender.sendToQueueSync 方法在特定套接字上同步发送数据。在这种情况下,我需要以某种方式传递socket 变量,以便sendData 知道这个变量。

SendRecord 类

public class SendRecord {
  private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
  private final Cache<Long, PendingMessage> cache = CacheBuilder.newBuilder().maximumSize(1000000)
      .concurrencyLevel(100).build();

  private static class Holder {
    private static final SendRecord INSTANCE = new SendRecord();
  }

  public static SendRecord getInstance() {
    return Holder.INSTANCE;
  }

  private SendRecord() {
    executorService.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        handleRetry();
      }
    }, 0, 1, TimeUnit.SECONDS);
  }

  private void handleRetry() {
    List<PendingMessage> messages = new ArrayList<>(cache.asMap().values());
    for (PendingMessage message : messages) {
      if (message.hasExpired()) {
        if (message.shouldRetry()) {
          message.markResent();
          doSendAsync(message);
        } else {
          cache.invalidate(message.getAddress());
        }
      }
    }
  }

  // called by multiple threads concurrently
  public boolean sendToQueueAsync(final long address, final byte[] encodedRecords) {
    PendingMessage m = new PendingMessage(address, encodedRecords, true);
    cache.put(address, m);
    return doSendAsync(m);
  }

  // called by above method and also by handleRetry method
  private boolean doSendAsync(final PendingMessage pendingMessage) {
    Optional<SocketHolder> liveSocket = SocketManager.getInstance().getNextSocket();
    ZMsg msg = new ZMsg();
    msg.add(pendingMessage.getEncodedRecords());
    try {
      // this returns instantly
      return msg.send(liveSocket.get().getSocket());
    } finally {
      msg.destroy();
    }
  }

  // called by send method below
  private boolean doSendAsync(final PendingMessage pendingMessage, final Socket socket) {
    ZMsg msg = new ZMsg();
    msg.add(pendingMessage.getEncodedRecords());
    try {
      // this returns instantly
      return msg.send(socket);
    } finally {
      msg.destroy();
    }
  }

  // called by multiple threads to send data synchronously without passing socket
  public boolean sendToQueueSync(final long address, final byte[] encodedRecords) {
    PendingMessage m = new PendingMessage(address, encodedRecords, false);
    cache.put(address, m);
    try {
      if (doSendAsync(m)) {
        return m.waitForAck();
      }
      return false;
    } finally {
      cache.invalidate(address);
    }
  }

  // called by a threads to send data synchronously but with socket as the parameter
  public boolean sendToQueueSync(final long address, final byte[] encodedRecords, final Socket socket) {
    PendingMessage m = new PendingMessage(address, encodedRecords, false);
    cache.put(address, m);
    try {
      if (doSendAsync(m, socket)) {
        return m.waitForAck();
      }
      return false;
    } finally {
      cache.invalidate(address);
    }
  }

  public void handleAckReceived(final long address) {
    PendingMessage record = cache.getIfPresent(address);
    if (record != null) {
      record.ackReceived();
      cache.invalidate(address);
    }
  }
}

调用者只会调用以下三种方法之一:

  • 通过传递两个参数sendToQueueAsync
  • 通过传递两个参数sendToQueueSync
  • 通过传递三个参数来发送ToQueueSync

我应该如何设计我的PacketSendRecord 类,以便我可以告诉Packet 类这些数据需要以上述三种方式之一发送到我的消息队列。由用户决定他想以哪种方式将数据发送到消息队列。截至目前我的Packet 类的结构方式,它只能以一种方式发送数据。

【问题讨论】:

    标签: java oop design-patterns bytebuffer single-responsibility-principle


    【解决方案1】:

    我认为您最好的选择是策略模式 (https://en.wikipedia.org/wiki/Strategy_pattern)。

    使用此模式,您可以封装每种“发送”的行为,例如,AsynchronousSend 类、SynchronousSend 类和 AsynchronousSocketSend 类。 (你可能会想出更好的名字)。然后Packet 类可以根据一些逻辑决定使用哪个类将数据发送到队列。

    【讨论】:

    • 三个不同的类发送相同的数据?你不觉得太多了吗?或者,也许我的想法是错误的。
    • @user1950349 是的,我不明白为什么不这样做。它使类具有单一的责任。小班总是更可取。
    • hmm 但是我将在Packet 类中添加什么逻辑来决定调用哪个代码来发送数据?
    • @user1950349 好吧,我不知道您需要什么逻辑,但我假设无论您选择使用哪种设计,您都需要相同的逻辑。
    【解决方案2】:

    我在Packet 中没有看到sender 的定义。我假设它被定义为私有实例变量?

    设计确实需要修复。 通过让Packet 类进行发送,该设计违反了Single responsibility principle。应该有一个单独的(可能是抽象的)类来准备要发送的数据(准备一个java.nio.Buffer 实例),它可以有一个或多个子类,其中一个返回一个java.nio.ByteBuffer 实例。

    获取Buffer 并执行发送的单独类。这个(可能是抽象的)类可以有用于不同发送平台和方法的子类。

    然后,您需要另一个实现Builder pattern 的类。希望发送数据包的客户端,使用构建器指定具体的PacketSender(可能还有其他需要的属性,如套接字号),然后调用send() 进行发送。

    【讨论】:

    • 是的,我省略了sender 的东西。一般来说,它可以是任何东西。具有三种不同方法来发送数据的类。就我而言,我有单例工厂类,我只是调用Sender.getInstance().sendToQueueAsync 方法,但我需要扩展它以便我可以调用任何我想要的方法?你能提供一个例子,以便我能正确理解吗?到目前为止有点困惑..
    【解决方案3】:

    你可以有一个枚举类,比如 PacketTransportionMode,它会为不同类型的枚举值(SYNC、ASYNC、SYNC_ON_SOCKET)覆盖一个“发送”方法,例如: .

    public enum PacketTransportionMode {
    SYNC {
        @Override
        public boolean send(Packet packet) {
            byte[] message = packet.getMessage();
            Socket socket = new Socket(packet.getReceiverHost(), packet.getReceiverPort());
            DataOutputStream dOut = new DataOutputStream(socket.getOutputStream());
            dOut.writeInt(message.length); // write length of the message
            dOut.write(message);           // write the message
            return true;
        }
    },
    ASYNC {
        @Override
        public boolean send(Packet packet) {
            // TODO Auto-generated method stub
            return false;
        }
    },
    SYNC_ON_SOCKET
    
    {
        @Override
        public boolean send(Packet packet) {
            // TODO Auto-generated method stub
            return false;
        }
    
    };
    public abstract boolean send(Packet packet);
    }
    

    另外,在包类中,引入 transportMode 变量。在 packet.send() 实现中,可以调用 this.packetTransportationMode.send(this)

    Client 可以在开始时创建数据包对象并设置它的 transportMode,类似于设置 RecordPartition。然后客户端可以调用 packet.send();

    或者除了将transportationMode变量放在packet类中调用this.packetTransportationMode.send(this),客户端也可以创建Packet对象直接调用PacketTransportionMode.SYNC.send(packet)。

    【讨论】:

    • 有趣的想法。您认为您可以在我的代码上提供示例基础,以便我更好地理解吗?到目前为止,我对这将如何工作感到有点困惑。
    【解决方案4】:

    首先,您需要对谁(或您的代码的哪一部分)负责决定使用哪种发送方法的问题有一个明确的答案。

    • 是否基于一些外部配置?
    • 是否基于某些(动态)用户决定?
    • 是否基于正在处理的分区?
    • 是否基于消息内容?

    (仅举几个可能性)

    答案将决定哪种结构最合适。

    尽管如此,很明显,当前的sendData() 方法是使决定生效的地方。因此,这个方法需要提供实现来使用。实际的send() 可能在所有情况下都是相似的。它建议将 sending 功能封装到一个提供send() 方法签名的接口中:

    send(address, data);
    

    如果要根据实际消息数据确定目标套接字,那么您可能更喜欢

    的一般签名
    send(address, data, socket);
    

    并使该套接字值可选或使用特定值来编码“无特定套接字”情况。否则,您可以使用特定的 Sender 实例,该实例具有通过构造函数传入的套接字。

    我目前没有从您提供的内容中看到一个正当的理由,即要求将三种不同的发送方法实现为一个类中的三种不同方法。如果公共代码是一个原因,那么使用公共基类将允许适当的共享。

    这就留下了一个问题,即如何在sendData() 中提供适当的Sender 实现的具体实例。

    如果要在sendData() 之外确定发送策略,则必须提交实现。作为参数或当前类实例的字段。如果本地数据决定了发送策略,您应该将正确实现的确定委托给将返回正确实现的选择类。然后调用将类似于:

    startegySelector.selectStartegy(selectionParameters).send(address,data);
    

    不过,如果没有更清楚地了解执行过程中什么是固定的,什么是可变的,那么很难提出最佳方法

    如果决策是基于数据的,则整个选择和转移过程是Packet类本地的。

    如果决定是在 Packet 外部做出的,您可能希望在该位置获取发送策略实现并将其作为参数传递给 addAndSendJunked()(或更准确地说是传递给 sendData()

    【讨论】:

    • 感谢您的建议。它始终基于用户决定,这意味着用户可以决定他们想要如何发送它。我需要有一种方法可以通过多种方式发送相同的数据,以便用户可以选择他们想要发送的方式。截至目前,在sendData 方法中,我通过调用此方法sendToQueueAsync 将所有内容作为异步发送,但总的来说,我需要一种可以通过3 种不同方式发送相同数据的方法。我的发件人类是thread safe singleton factory,使用这三种方法将数据发送到消息队列。
    【解决方案5】:

    使用枚举变量来定义发送消息的类型

    public enum TypeToSend {
        async, sync, socket 
    }
    
    public final class Packet implements Closeable {
    TypeToSend typeToSend;
    public Packet(TypeToSend typeToSend) {
            this.typeToSend = typeToSend;
        }
    switch(typeToSend){
         case async:{}
         case sync:{}
         case socket:{}
    }
    }
    

    【讨论】:

      【解决方案6】:

      战略。与 Kerri Brown 的回答不同的是,Packet 不应该在策略之间做出决定。相反,在 Packet 类之外决定它。

      单个发送策略接口应该由 3 个不同的类实现,每个类对应于上述发送方法中的一种。将策略接口注入到Packet中,使Packet无论处理哪种策略都不必改变。

      你说它必须基于用户的选择。所以你可以先问用户,选择是什么,然后在此基础上,实例化一个与用户选择相对应的发送策略接口的实现。然后,用选定的发送策略实例实例化 Packet。

      如果您觉得以后的选择可能不取决于用户,那么将其设为工厂。那么你的解决方案就变成了工厂和策略的结合。

      在这种情况下,Packet 可以注入 Factory 接口。 Packet 要求 Factory 给它发送策略。接下来,它使用从工厂获得的策略发送。工厂要求用户输入,稍后可以通过基于其他条件而不是用户输入的选择来代替。您可以通过在未来以不同的方式实现工厂接口并注入新工厂而不是这个工厂来实现这一点(即基于用户输入的工厂与其他基于条件的工厂)。

      这两种方法都会为您提供遵循打开/关闭原则的代码。但是,如果您真的不需要工厂,请尽量不要过度设计。

      【讨论】:

        猜你喜欢
        • 2016-05-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-21
        • 2016-08-29
        • 2022-01-14
        • 1970-01-01
        相关资源
        最近更新 更多