1.什么是GRPC
GRPC是RPC框架中的一种,是一个高性能,开源和通用的RPC框架,基于Protobuf序列化协议开发,且支持众多开发语言。
面向服务端和协议端,基于http/2设计,带来诸如双向流,流控,头部压缩,单TCP连接上的多路复用请求等特性。这些特性使得其在移动设备上表现的更好,更省电和节省空间。
在GRPC里客户端可以向调用本地对象一样直接调用另一台不同机器上服务端医用的方法,使得您能够更容易地创建分布式应用和服务。
与许多RPC系统类似,GRPC也是基于以下理念:定义一个服务,指定其能够被远程调用的方法。在服务端实现这个接口。并运行一个GRPC服务器来处理客户端调用。在客户端拥有一个存根能够向服务端一样的方法。
2.GRPC特性以及应用场景
(1)特性:
grpc可以跨语言使用。支持多种语言 支持C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP等编程语言。
基于 IDL ( 接口定义语言)文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub。
通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量。
序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
- 安装简单,扩展方便(用该框架每秒可达到百万个RPC)
(2)使用场景:
- 微服务:gRPC 设计用于低延迟和高吞吐量通信。 gRPC 对于效率至关重要的轻量级微服务非常有用。
- 点对点实时通信:gRPC 对双向流式传输提供出色的支持。 gRPC 服务可以实时推送消息而无需轮询。
- 多语言环境:gRPC 工具支持所有常用的开发语言,因此,gRPC 是多语言环境的理想选择。
- 网络受限环境:gRPC 消息使用 Protobuf(一种轻量级消息格式)进行序列化。 gRPC 消息始终小于等效的 JSON 消息。
3.GRPC大致请求流程
- 客户端(gRPC Stub)调用 A 方法,发起 RPC 调用。
- 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)。
- 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回。
- 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)。
- 客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果。
4.GRPC的优点和缺点
- 优点:
- protobuf二进制消息,性能好/效率高
- proto文件生成目标代码,简单易用
- 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射
- 支持向前兼容和向后兼容
- 支持多种语言,底层采用Netty实现
- 缺点:
- GRPC尚未提供链接池,需要自己实现。
- 尚未提供服务发现、负载均衡机制
- Protobuf二进制可读性差
5.SpringBoot整合GRPC环境准备
(1)案例背景
统一下单业务,下单时会选择对应的商品和优惠券,那么根据选择的商品ID和优惠券ID分别去商品微服务和优惠券微服务进行调用。
(2)创建MAVEN聚合项目
(3)父级工程pom.xml引入依赖,锁定版本
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <!-- grpc版本--> <grpc-version>1.42.2</grpc-version> <!-- service和client要使用的lib版本--> <common-version>1.0-SNAPSHOT</common-version> <!-- netty版本--> <netty-version>4.1.65.Final</netty-version> <!-- Springboot版本--> <spring-boot.version>2.6.4</spring-boot.version> <!-- Springboot-grpc版本,用于server服务注解使用--> <grpc-spring-boot-starter.version>2.13.1.RELEASE</grpc-spring-boot-starter.version> <!-- maven构建工具版本--> <maven-plugin-version>3.8.1</maven-plugin-version> <!-- lombok--> <lombok-version>1.18.16</lombok-version> <!--fastjson--> <fastjson.version>1.2.83</fastjson.version> </properties> <!--使用dependencyManagement声明得到依赖子模块不需要再进行版本指定,直接通过父模块指定即可,以此实现依赖的统一管理,防止出现依赖冲突--> <dependencyManagement> <dependencies> <dependency> <groupId>com.lixiang</groupId> <artifactId>common</artifactId> <version>${common-version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty</artifactId> <version>${grpc-version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>${grpc-version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>${grpc-version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-common</artifactId> <version>${netty-version}</version> </dependency> <!-- spring boot grpc 依赖 --> <dependency> <groupId>net.devh</groupId> <artifactId>grpc-client-spring-boot-starter</artifactId> <version>${grpc-spring-boot-starter.version}</version> </dependency> <dependency> <groupId>net.devh</groupId> <artifactId>grpc-server-spring-boot-starter</artifactId> <version>${grpc-spring-boot-starter.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok-version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> </dependencies> </dependencyManagement>
(4)创建common模块
common中引入依赖
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <os.detected.classifier>windows-x86_64</os.detected.classifier> </properties> <dependencies> <!-- Spring--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty</artifactId> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-common</artifactId> </dependency> <dependency> <groupId>net.devh</groupId> <artifactId>grpc-client-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>net.devh</groupId> <artifactId>grpc-server-spring-boot-starter</artifactId> </dependency> <!-- other--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <!--配置protobuf插件 可参阅https://github.com/grpc/grpc-java--> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.0</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.19.1:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.43.1:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-plugin-version}</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build>
这块注意这个值os.detected.classifier,获取当前系统信息,根据你们自己的电脑或者运行环境去填写,获取信息的主类如下:
public class GetClassifier { public static void main(String[] args) { System.out.println(System.getProperty("os.name")); System.out.println(System.getProperty("os.arch")); } }
common模块中创建一个统一返回工具类
/** * @description 统一返回格式工具类 * @author lixiang * @Version 1.0 */ @Data @AllArgsConstructor @NoArgsConstructor public class BaseResponse { /** * 状态码 0 表示成功 */ private Integer code; /** * 数据 */ private Object data; /** * 描述 */ private String msg; /** * 获取远程调用数据 * 注意事项:支持多单词下划线专驼峰(序列化和反序列化) * @param typeReference * @param <T> * @return */ public <T> T getData(TypeReference<T> typeReference){ return JSON.parseObject(JSON.toJSONString(data),typeReference); } /** * 成功,不传入数据 * @return */ public static BaseResponse buildSuccess() { return new BaseResponse(0, null, null); } /** * 成功,传入数据 * @param data * @return */ public static BaseResponse buildSuccess(Object data) { return new BaseResponse(0, data, null); } /** * 失败,传入描述信息 * @param msg * @return */ public static BaseResponse buildError(String msg) { return new BaseResponse(-1, null, msg); } /** * 自定义状态码和错误信息 * @param code * @param msg * @return */ public static BaseResponse buildCodeAndMsg(int code, String msg) { return new BaseResponse(code, null, msg); } }
添加依赖
<dependencies> <dependency> <groupId>com.lixiang</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-plugin-version}</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>com.lixiang.CouponApplication</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
创建SpringBoot运行主类
@SpringBootApplication public class CouponApplication { public static void main(String[] objArgs) { SpringApplication.run(CouponApplication.class, objArgs); } }
创建application.properties
# 定义服务名 spring.application.name=coupon-server # 定义服务端口 server.port=8081 # 定义GRPC端口 grpc.server.port=8071
测试启动
(6)创建product-server模块
添加依赖
<dependencies> <dependency> <groupId>com.lixiang</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-plugin-version}</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>com.lixiang.ProductApplication</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
创建SpringBoot运行主类
@SpringBootApplication public class ProductApplication { public static void main(String[] objArgs) { SpringApplication.run(ProductApplication.class, objArgs); } }
创建application.properties
# 定义服务名 spring.application.name=product-server # 定义服务端口 server.port=8083 # 定义GRPC端口 grpc.server.port=8073
测试运行
这里我们需要注意一点,我们在优惠券服务和商品服务都定义了一个grpc.server.port,这个是用来提供给其他服务调用是写的端口,不要和springboot本身的server.port重复,否则会报错。
(7)创建order-server模块
添加maven依赖
<dependencies> <dependency> <groupId>com.lixiang</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-plugin-version}</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>com.lixiang.OrderApplication</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
创建SpringBoot运行主类
@SpringBootApplication public class OrderApplication { public static void main(String[] objArgs) { SpringApplication.run(OrderApplication.class, objArgs); } }
创建application.properties
# 定义服务名 spring.application.name=order-server # 定义服务端口 server.port=8082 # 定义GRPC端口 grpc.server.port=8072 # 定义调用商品服务的GRPC product.server.address=localhost:8073 # 定义调用优惠券服务的GRPC coupon.server.address=localhost:8071
6.SpringBoot整合GRPC业务开发
ok,到目前我们所有的环境准备已经完成了,那么下面我们就开始进入到业务的开发,首先我们在下单的时候,会传入一个商品ID和一个优惠券ID,
那么我们要通过这两个ID去对应的服务查出具体的详细信息。
(1)开发coupon-server根据ID获取优惠卷详情信息
准备CouponServer.proto文件,写在common模块中
CouponServer.proto内容如下:
/** * 编译工具版本 */ syntax = "proto3"; /** * 指定生成实体 */ option java_multiple_files = true; /** * 指定生成接口 */ option java_generic_services = true; /** * 声明包 */ package com.lixiang.grpc.server; /** * 商品服务proto文件 */ option java_outer_classname = "CouponServer"; /** * 统一返回实体 */ message CouponServerResponse { string message = 1; int32 code = 2; string data=3; } /** * 声明接口 */ service CouponService { rpc deductProductInventory (DeductCouponRequest) returns (CouponServerResponse); } /** * 声明扣减商品库存实体 */ message DeductCouponRequest { int32 couponId = 1; }
编写好proto文件点开maven运行protobuf插件
运行好之后,会发现target下多了一个这个包
然后在coupon-server中开始编写获取优惠券的方法,这块我们先模拟一些优惠券的数据。
/** * 优惠券实体类 */ @Data public class Coupon { /** * 优惠券ID */ private Integer id; /** * 优惠券金额 */ private BigDecimal amount; /** * 满减额度 */ private BigDecimal fullReduction; /** * 优惠券名称 */ private String name; /** * 优惠券可用开始时间 */ private LocalDateTime startTime; /** * 优惠券结束时间 */ private LocalDateTime endTime; }
public class CouponUtil { private final static List<Coupon> couponList = new ArrayList<>(); static { Coupon coupon1 = new Coupon(); coupon1.setAmount(new BigDecimal(50)); coupon1.setFullReduction(new BigDecimal(300)); coupon1.setId(1); coupon1.setName("满300减50券"); coupon1.setStartTime(LocalDateTime.parse("2022-10-20 16:25:49", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); coupon1.setEndTime(LocalDateTime.parse("2023-10-20 16:25:49", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); Coupon coupon2 = new Coupon(); coupon2.setAmount(new BigDecimal(100)); coupon1.setFullReduction(new BigDecimal(500)); coupon2.setId(2); coupon2.setName("满500减100券"); coupon2.setStartTime(LocalDateTime.parse("2022-10-20 16:25:49", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); coupon2.setEndTime(LocalDateTime.parse("2023-10-20 16:25:49", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); Coupon coupon3 = new Coupon(); coupon3.setAmount(new BigDecimal(200)); coupon1.setFullReduction(new BigDecimal(800)); coupon3.setId(3); coupon3.setName("满800减100券"); coupon3.setStartTime(LocalDateTime.parse("2022-10-20 16:25:49", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); coupon3.setEndTime(LocalDateTime.parse("2023-10-20 16:25:49", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); couponList.add(coupon3); couponList.add(coupon2); couponList.add(coupon1); } /** * 获取具体某个优惠券 * @param couponId * @return */ public static Coupon getCouponById(Integer couponId){ return couponList.stream().filter(obj-> Objects.equals(obj.getId(), couponId)).collect(Collectors.toList()).get(0); } }
创建CouponGrpcServer,继承CouponServiceGrpc.CouponServiceImplBase,CouponServiceGrpc.CouponServiceImplBase这个类是刚刚运行插件自动生成的。
@GrpcService public class CouponGrpcServer extends CouponServiceGrpc.CouponServiceImplBase { @Override public void deductProductInventory(DeductCouponRequest request, StreamObserver<CouponServerResponse> responseObserver) { int couponId = request.getCouponId(); //查找优惠券详细信息 Coupon coupon = CouponUtil.getCouponById(couponId); String jsonData = JSON.toJSONString(coupon); CouponServerResponse couponServerResponse = CouponServerResponse.newBuilder() .setCode(200) .setMessage("") .setData(jsonData) .build(); responseObserver.onNext(couponServerResponse); responseObserver.onCompleted(); }
ok,这样我们一个根据ID查询优惠券的信息就定义好了。
整体的目录结构:
(2)开发product-server根据ID获取优惠卷详情信息
product-server和coupon-server是一样的,这里就不一一细说了,直接上代码。
/** * 商品实体类 */ @Data public class Product { /** * 商品ID */ private Integer id; /** * 商品名称 */ private String name; /** * 商品价格 */ private BigDecimal price; }
public class ProductUtil { private final static List<Product> productList = new ArrayList<>(); static { Product product1 = new Product(); product1.setId(1); product1.setName("互联网JAVA架构师养成零基础到精通"); product1.setPrice(new BigDecimal(12999)); Product product2 = new Product(); product2.setId(2); product2.setName("Python+大数据零基础到精通"); product2.setPrice(new BigDecimal(15999)); Product product3 = new Product(); product3.setId(3); product3.setName("5G云计算运维架构零基础到精通"); product3.setPrice(new BigDecimal(10999)); productList.add(product1); productList.add(product2); productList.add(product3); } /** * 根据商品ID获取商品详情 * @param productId * @return */ public static Product getProductById(Integer productId){ return productList.stream().filter(obj-> Objects.equals(obj.getId(), productId)).collect(Collectors.toList()).get(0); } }
@GrpcService public class ProductGrpcServer extends ProductServiceGrpc.ProductServiceImplBase { @Override public void deductProductInventory(DeductInventoryRequest request, StreamObserver<ProductServerResponse> responseObserver) { int productId = request.getProductId(); Product product = ProductUtil.getProductById(productId); String jsonData = JSON.toJSONString(product); ProductServerResponse productServerResponse = ProductServerResponse.newBuilder() .setCode(200) .setMessage("") .setData(jsonData) .build(); responseObserver.onNext(productServerResponse); responseObserver.onCompleted(); } }
目录结构:
(3)开发order-server模块统一下单接口
首先我们要先配置一下GRPC的地址
配置GrpcClientConfig
/** * @description Grpc Client 配置类 * @author lixiang */ @Configuration public class GrpcClientConfig { /** * 商品服务地址 */ @Value("${product.server.address}") private String productServerAddress; /** * 优惠券服务地址 */ @Value("${coupon.server.address}") private String couponServerAddress; /** * 商品服务grpc-client * @return */ @Bean public ProductServiceGrpc.ProductServiceBlockingStub getProductServerClient() { return ProductServiceGrpc.newBlockingStub(ManagedChannelBuilder.forTarget(productServerAddress).usePlaintext().build()); } /** * 优惠卷服务grpc-client * @return */ @Bean public CouponServiceGrpc.CouponServiceBlockingStub getCouponServerClient() { return CouponServiceGrpc.newBlockingStub(ManagedChannelBuilder.forTarget(couponServerAddress).usePlaintext().build()); } }
编写统一下单接口请求类
@Data public class OrderConfirmRequest { /** * 支付价格 */ private BigDecimal totalAmount; /** * 支付类型 */ private String payType; /** * 支付商品的ID */ private Integer productId; /** * 支付时用的优惠券ID */ private Integer couponRecordId; }
编写统一下单接口
@RestController @RequestMapping(value = "/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/confirm") public BaseResponse confirm(@RequestBody OrderConfirmRequest orderConfirmRequest) { orderService.confirm(orderConfirmRequest); return BaseResponse.buildSuccess(); } }
编写统一下单业务类
public interface OrderService { /** * 统一下单接口 * @param orderConfirmRequest */ void confirm(OrderConfirmRequest orderConfirmRequest); }
@Service @Slf4j public class OrderServiceImpl implements OrderService{ @Autowired private ProductServiceGrpc.ProductServiceBlockingStub productService; @Autowired private CouponServiceGrpc.CouponServiceBlockingStub couponService; @Override public void confirm(OrderConfirmRequest orderConfirmRequest) { //1.调用优惠券服务获取优惠券详情 DeductCouponRequest deductCouponRequest = DeductCouponRequest.newBuilder() .setCouponId(orderConfirmRequest.getCouponRecordId()) .build(); CouponServerResponse couponServerResponse = couponService.deductProductInventory(deductCouponRequest); String couponDataStr = couponServerResponse.getData(); JSONObject couponData = JSON.parseObject(couponDataStr); log.info("调用优惠卷服务获取的优惠券信息:{}",couponData); //2.调用商品服务获取商品详细信息 DeductInventoryRequest deductInventoryRequest = DeductInventoryRequest.newBuilder() .setProductId(orderConfirmRequest.getProductId()) .build(); ProductServerResponse productServerResponse = productService.deductProductInventory(deductInventoryRequest); String productDataStr = productServerResponse.getData(); JSONObject productData = JSON.parseObject(productDataStr); log.info("调用商品服务获取的商品信息:{}",productData); //3.判断优惠券是否在使用时间范围内 long today = new Date().getTime(); long startTime = couponData.getDate("startTime").getTime(); long endTime = couponData.getDate("endTime").getTime(); if(startTime>today || endTime<today){ throw new RuntimeException("当前优惠券不在可用范围内"); } //4.验证价格 BigDecimal amount = couponData.getBigDecimal("amount"); BigDecimal price = productData.getBigDecimal("price"); if(!price.subtract(amount).equals(orderConfirmRequest.getTotalAmount())){ throw new RuntimeException("订单验价失败"); } //5.生成订单 log.info("当前订单购买的商品为:{},原价为:{},本次消耗优惠券:{},实际支付金额:{}", productData.getString("name"),productData.getBigDecimal("price"),couponData.getString("name"),orderConfirmRequest.getTotalAmount()); } }
测试全流程,启动三个微服务
至此,由GRPC整合微服务,实现远程通信就已经完成了,当然下单业务不止这么简单,中间还有防重提交,调用三方支付,延时关单等等一些列复杂的业务,这里只是给大家演示一下怎末用GRPC代替feign实现微服务之间的远程通信。