簡介
gRPC(Google 遠端程序呼叫)使用 協定緩衝區 (Protobuf) 進行二進位序列化,並使用 HTTP/2 進行傳輸。與 REST/JSON 相比,gRPC 在序列化和頻寬方面快 5-10 倍。 Quarkus 將 gRPC 與自動程式碼產生集成 .proto 文件。
何時使用 gRPC 而不是 REST?
| 標準 | 休息/JSON | gRPC/Protobuf |
|---|---|---|
| 性能 | 較慢(基於文字) | 快 5-10 倍 |
| 有效負載大小 | 大(JSON 詳細) | 小(二進位) |
| 合約 | OpenAPI(可選) | Protobuf(必要) |
| 串流媒體 | WebSocket/SSE | 內建(4 種模式) |
| 瀏覽器支援 | 本地 | 需要 gRPC-Web 代理 |
| 調試 | 簡單(捲曲,郵差) | 困難(需要單獨的工具) |
| 適合 | 公共 API、Web 應用程式 | 內部服務 |
在電子商務專案中:gRPC 用於內部服務到服務,REST 用於外部/前端。
在 Quarkus 中設定 gRPC
依賴關係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
Protobuf 定義
// src/main/proto/product_service.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.xdev.ecommerce.product.grpc";
package product;
service ProductGrpcService {
// Unary RPC
rpc GetProduct (GetProductRequest) returns (ProductResponse);
rpc CheckStock (CheckStockRequest) returns (StockResponse);
rpc ReserveStock (ReserveStockRequest) returns (StockResponse);
// Server Streaming
rpc ListProducts (ListProductsRequest)
returns (stream ProductResponse);
// Client Streaming (batch import)
rpc BatchUpdateStock (stream UpdateStockRequest)
returns (BatchStockResponse);
}
message GetProductRequest {
int64 product_id = 1;
}
message ProductResponse {
int64 id = 1;
string name = 2;
string description = 3;
string price_amount = 4; // BigDecimal as string
string currency = 5;
int32 stock_available = 6;
string category_name = 7;
string status = 8;
}
message CheckStockRequest {
int64 product_id = 1;
int32 quantity = 2;
}
message StockResponse {
bool available = 1;
int32 current_stock = 2;
string message = 3;
}
message ReserveStockRequest {
int64 product_id = 1;
int32 quantity = 2;
string order_id = 3;
}
message ListProductsRequest {
string category = 1;
int32 page = 2;
int32 size = 3;
}
message UpdateStockRequest {
int64 product_id = 1;
int32 quantity_change = 2; // positive = add, negative = reduce
}
message BatchStockResponse {
int32 success_count = 1;
int32 failure_count = 2;
repeated string errors = 3;
}
gRPC 伺服器實現
import io.quarkus.grpc.GrpcService;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
@GrpcService
public class ProductGrpcServiceImpl
implements ProductGrpcService {
@Inject
ProductRepository productRepo;
@Override
public Uni<ProductResponse> getProduct(
GetProductRequest request) {
return Uni.createFrom().item(() -> {
Product product = productRepo
.findByIdOptional(request.getProductId())
.orElseThrow(() -> new StatusRuntimeException(
Status.NOT_FOUND.withDescription(
"Product " + request.getProductId()
+ " not found")));
return toResponse(product);
});
}
@Override
public Uni<StockResponse> checkStock(
CheckStockRequest request) {
return Uni.createFrom().item(() -> {
Product product = productRepo
.findByIdOptional(request.getProductId())
.orElseThrow(() -> new StatusRuntimeException(
Status.NOT_FOUND));
boolean available =
product.stock.available() >= request.getQuantity();
return StockResponse.newBuilder()
.setAvailable(available)
.setCurrentStock(product.stock.available())
.setMessage(available
? "Stock available"
: "Insufficient stock")
.build();
});
}
@Override
@Transactional
public Uni<StockResponse> reserveStock(
ReserveStockRequest request) {
return Uni.createFrom().item(() -> {
Product product = productRepo
.findByIdOptional(request.getProductId())
.orElseThrow(() -> new StatusRuntimeException(
Status.NOT_FOUND));
product.reserveStock(request.getQuantity());
return StockResponse.newBuilder()
.setAvailable(true)
.setCurrentStock(product.stock.available())
.setMessage("Stock reserved for order "
+ request.getOrderId())
.build();
});
}
@Override
public Multi<ProductResponse> listProducts(
ListProductsRequest request) {
return Multi.createFrom().items(() -> {
var query = productRepo.findActive(
request.getCategory(), null,
Sort.by("createdAt").descending());
return query.page(Page.of(
request.getPage(), request.getSize()))
.list().stream()
.map(this::toResponse);
});
}
private ProductResponse toResponse(Product p) {
return ProductResponse.newBuilder()
.setId(p.id)
.setName(p.name)
.setDescription(
p.description != null ? p.description : "")
.setPriceAmount(
p.price != null
? p.price.amount().toPlainString() : "0")
.setCurrency(
p.price != null ? p.price.currency() : "VND")
.setStockAvailable(
p.stock != null ? p.stock.available() : 0)
.setCategoryName(
p.category != null ? p.category.name : "")
.setStatus(p.status)
.build();
}
}
設定 gRPC 伺服器
# gRPC server port
quarkus.grpc.server.port=9000
# Cùng host với REST (dev)
quarkus.grpc.server.use-separate-server=true
# TLS (production)
%prod.quarkus.grpc.server.ssl.certificate=server.crt
%prod.quarkus.grpc.server.ssl.key=server.key
訂單服務中的 gRPC 用戶端
依賴關係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
複製原型文件
放在一起 .proto 文件輸入 order-service/src/main/proto/。
客戶端配置
# application.properties
quarkus.grpc.clients.product-grpc.host=localhost
quarkus.grpc.clients.product-grpc.port=9000
使用 gRPC 用戶端
import io.quarkus.grpc.GrpcClient;
@ApplicationScoped
public class OrderService {
@GrpcClient("product-grpc")
ProductGrpcService productGrpc;
@Transactional
public OrderDTO createOrder(String customerId,
CreateOrderRequest request) {
Order order = new Order();
order.customerId = customerId;
for (var item : request.items()) {
// gRPC call thay REST
ProductResponse product = productGrpc
.getProduct(GetProductRequest.newBuilder()
.setProductId(item.productId())
.build())
.await().indefinitely();
// Check stock qua gRPC
StockResponse stock = productGrpc
.checkStock(CheckStockRequest.newBuilder()
.setProductId(item.productId())
.setQuantity(item.quantity())
.build())
.await().indefinitely();
if (!stock.getAvailable()) {
throw new BusinessException(400,
stock.getMessage());
}
order.addItem(product.getId(), product.getName(),
Money.vnd(new BigDecimal(
product.getPriceAmount())),
item.quantity());
}
// Reserve stock
for (OrderItem oi : order.items) {
productGrpc.reserveStock(
ReserveStockRequest.newBuilder()
.setProductId(oi.productId)
.setQuantity(oi.quantity)
.setOrderId(order.orderNumber)
.build())
.await().indefinitely();
}
order.persist();
return OrderDTO.from(order);
}
}
gRPC 異常處理
import io.grpc.StatusRuntimeException;
import io.grpc.Status;
try {
ProductResponse product = productGrpc
.getProduct(request)
.await().indefinitely();
} catch (StatusRuntimeException e) {
switch (e.getStatus().getCode()) {
case NOT_FOUND ->
throw new ResourceNotFoundException(
"Product", productId);
case UNAVAILABLE ->
throw new ServiceUnavailableException(
"Product Service unavailable");
case DEADLINE_EXCEEDED ->
throw new BusinessException(504,
"Product Service timeout");
default ->
throw new RuntimeException(
"gRPC error: " + e.getStatus());
}
}
gRPC 截止時間(超時)
import java.time.Duration;
import io.smallrye.mutiny.Uni;
ProductResponse product = productGrpc
.getProduct(request)
.ifNoItem().after(Duration.ofSeconds(5))
.fail()
.onFailure(TimeoutException.class)
.recoverWithItem(() -> {
// Fallback hoặc cached response
return ProductResponse.getDefaultInstance();
})
.await().indefinitely();
練習
- 創建
product_service.proto使用 GetProduct、CheckStock、ReserveStock RPC - 實施
ProductGrpcServiceImpl在產品服務中 - 在 Order Service 中建立一個名為 Product Service 的 gRPC 用戶端
- 為 ListProducts 實作伺服器串流傳輸
- 新增帶有狀態代碼的 gRPC 異常處理
- 比較 REST 和 gRPC 呼叫之間的回應時間(基準)
總結
- gRPC 使用 Protobuf 二進位序列化 — 比 REST/JSON 快 5-10 倍
.protofile — 合約優先,自動程式碼生成@GrpcService— 註解伺服器實現@GrpcClient— 注入 gRPC 用戶端存根- 4 種模式:一元、伺服器流、客戶端流、雙向
- gRPC 用於內部通信,REST 用於外部/公共 API
下一篇:Apache Kafka — 事件驅動架構。