Hình dung việc gửi một object cho verticle khác như đưa cho đồng nghiệp một tài liệu. Bạn có thể đưa bản photocopy — họ tha hồ gạch xoá, bản gốc của bạn vẫn nguyên; hoặc đưa thẳng bản gốc duy nhất để đỡ tốn công copy — nhanh hơn thật, nhưng họ sửa một dòng là tài liệu của bạn cũng đổi theo, và nếu cả hai cùng viết lên nó một lúc thì loạn. Ba cách đóng gói object lên event bus của Vert.x chính là ba mức trên cái phổ "copy hay đưa bản gốc" đó. Phần trước gửi toàn chuỗi. Ứng dụng thật gửi đối tượng, và event bus không tự biết cách đóng gói chúng.

Không có codec thì không gửi được

eb.send("don", new DonHang("DH-1", 250000, "..."));
IllegalArgumentException: No message codec for type: class lab.Codec$DonHang

Ném ngay, đồng bộ, tại chỗ gửi. Đây là một trong số ít lỗi của Vert.x nói thẳng vấn đề là gì — tận dụng điều đó và đừng vội tìm cách đi vòng.

Ba cách đi tiếp, và chúng khác nhau nhiều hơn vẻ ngoài.

Cách 1: chuyển sang JsonObject

eb.send("don", JsonObject.mapFrom(don));

Không cần khai gì. Đây là cách gần như ai cũng chọn đầu tiên, và với phần lớn ứng dụng thì nó đủ tốt.

Cách 2: codec nhị phân tự viết

class NhiPhan implements MessageCodec<DonHang, DonHang> {
    public void encodeToWire(Buffer b, DonHang d) {
        b.appendInt(d.ma().length()).appendString(d.ma());
        b.appendInt(d.tien());
        ...
    }
    public DonHang decodeFromWire(int pos, Buffer b) { ... }
    public DonHang transform(DonHang d) { return d; }
    public String name() { return "nhi-phan"; }
    public byte systemCodecID() { return -1; }
}
eb.registerDefaultCodec(DonHang.class, new NhiPhan());

Đo trên một bản ghi mẫu:

Kích thước trên dây Thông lượng (giao đủ 200 000)
JsonObject 86 byte 2 749 569 /giây
Codec nhị phân 65 byte (76%) 3 429 083 /giây

Gọn hơn 24%, nhanh hơn 25%. Đáng làm không? Với hầu hết hệ thống thì không — cả hai đều ở mức hàng triệu thông điệp mỗi giây, cao hơn nhiều lần thông lượng HTTP mà phần 16 đo được. Bạn đang tối ưu thứ không phải nút thắt.

Codec tự viết đáng làm khi thông điệp lớn hoặc rất nhiều, hoặc khi bạn cần một định dạng cố định để nói chuyện với hệ thống khác.

Cách 3: codec cục bộ, và điều nó thật sự làm

class CucBo implements MessageCodec<DonHang, DonHang> {
    public void encodeToWire(Buffer b, DonHang d) { throw new UnsupportedOperationException(); }
    public DonHang decodeFromWire(int pos, Buffer b) { throw new UnsupportedOperationException(); }
    public DonHang transform(DonHang d) { return d; }        // tra ve CHINH doi tuong do
    ...
}
bên nhận có đúng ĐỐI TƯỢNG đó không: CÓ (cùng tham chiếu)

Không tuần tự hoá, không sao chép, không cấp phát. transform là phương thức Vert.x gọi khi người gửi và người nhận ở cùng JVM, và trả về chính đối tượng đó nghĩa là thông điệp đi qua bus với chi phí bằng một lời gọi hàm.

Và đó cũng là cái bẫy. Bên nhận cầm chính object của bên gửi. Nếu nó sửa một trường, bên gửi thấy thay đổi đó — hai verticle chạy trên hai event loop khác nhau vừa chia sẻ trạng thái, đúng thứ mà mô hình nhắn tin sinh ra để tránh. Mọi phép suy luận "verticle của tôi chỉ một luồng chạm vào" sụp đổ tại đây.

Đây đúng là "đưa bản gốc thay vì bản photocopy". Dùng codec cục bộ thì object phải bất biến. Với record của Java thì việc đó gần như miễn phí — và đó là lý do mọi ví dụ trong bài này dùng record.

Thêm một điểm nữa: encodeToWire ném ngoại lệ nghĩa là thông điệp không đi qua mạng được. Ngày ai đó bật cụm (bài 21), mọi thông điệp phải sang node khác sẽ hỏng — và nó hỏng lúc chạy, không phải lúc biên dịch.

Chọn cách nào

Tình huống Cách
Bình thường, chưa đo thấy vấn đề JsonObject
Nhiều verticle trong cùng JVM, object bất biến Codec cục bộ
Thông điệp lớn, hoặc chạy trên cụm, hoặc cần định dạng cố định Codec nhị phân
Cần nói chuyện với hệ thống viết bằng ngôn ngữ khác JsonObject — hoặc một định dạng chuẩn

Dòng cuối đáng nhấn: sê-ri RabbitMQ đo được rằng gắn tên class Java vào thông điệp trói hai dịch vụ vào nhau. Codec tự viết ở đây có cùng rủi ro nếu bạn coi nó là hợp đồng giữa các dịch vụ — hãy giữ nó cho giao tiếp bên trong một ứng dụng.

Muốn biết codec của mình có thật sự không sao chép hay không thì kiểm một dòng:

// kiem xem codec cua ban co thuc su khong sao chep khong
eb.consumer("thu", (Message<DonHang> m) ->
    System.out.println("cung tham chieu: " + (m.body() == donGoc)));

Ra true nghĩa là bạn đang chia sẻ object giữa hai verticle. Chấp nhận được nếu nó bất biến — và là một lỗi đang chờ xảy ra nếu không.

Mẫu số chung

Sự an toàn của mô hình nhắn tin không đến từ cái bus, nó đến từ cái copy — mỗi bên làm việc trên bản của riêng mình nên không ai giẫm lên trạng thái của ai. Codec cục bộ tối ưu bằng cách bỏ luôn cái copy đó, và thế là nó lén đặt một object khả biến lên bàn của hai luồng — làm sống lại đúng cái shared-mutable-state mà cả mô hình sinh ra để giết. Đây là ranh giới ai làm đồng thời cũng gặp: actor của Erlang/Akka sao chép thông điệp; Go dạy "đừng giao tiếp bằng chia sẻ bộ nhớ, hãy chia sẻ bộ nhớ bằng giao tiếp" — và cái cửa hậu (nhét một con trỏ vào channel) mang y hệt cái bẫy này. Lối thoát sạch là tính bất biến: một record/val/object đóng băng thì đưa-bản-gốc cũng an toàn như đưa-bản-copy, vì không ai làm hỏng được thứ không ai sửa được. Tốc độ của pass-by-reference chỉ miễn phí khi dữ liệu không đổi.

Điều thứ hai: hãy tối ưu cái nút thắt bạn đã đo, đừng tối ưu cái bạn tưởng. Codec nhị phân nhanh hơn 25% nghe hấp dẫn, nhưng cả hai cách đều đang chạy hàng triệu thông điệp mỗi giây trong khi trần thật của hệ là tầng HTTP chậm hơn nhiều bậc — công sức đổ vào đây là công sức phí. Và coi chừng thứ tối ưu chỉ đúng trong một kiểu triển khai: encodeToWire ném ngoại lệ chạy ngon trên một JVM, rồi gãy lúc chạy đúng ngày ai đó bật cụm — một quả mìn nằm im, không lộ lúc biên dịch, chỉ nổ khi môi trường đổi. Nhanh hơn mà mất tính đúng khi triển khai khác đi thì không phải là nhanh hơn, chỉ là hoãn một sự cố sang chỗ khó tìm hơn.

Bài sau: so event bus với gọi HTTP nội bộ, bằng cùng một phép đo.