项目git地址:https://gitee.com/lpengs/cloud-demo.git

声明:笔记来源于网络,如有侵权联系删除

1.Gateway-网关

当前端调用接口时,由于很多微服务的地址都不一样,而暴露给前端的往往就是一个ip+端口。这时候,可以利用网关,根据请求地址中的路径来分别调用不同的微服务,比如/order,调用订单微服务,/product,调用商品微服务,这就是请求路由的功能。在网关调用微服务时,可以加入负载

均衡、流量控制、身份认证、协议转换、系统监控、安全防护等功能。 

Spring Cloud Gateway网关一般有上面两种,Reactive Server和Server MVC。 

Reactive Server可以占用少量资源实现高并发。Server MVC是传统的网关。我们推荐用Reactive Server。

问题1:客户端发送/api/order/** 转到service-order

问题2:客户端发送/api/product/**转到service-product

问题3:以上转发具有负载均衡效果

1)创建网关

1.创建网关对应的代码

鼠标右键根目录->New->Module

创建gateway模块

引入依赖,然后右上角点击刷新

    <dependencies>
        <!--服务发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>

 

创建启动类:gateway->java->New->Java Class

 输入com.atguigu.gateway.GetwayMainApplication,回车

@EnableDiscoveryClient
@SpringBootApplication
public class GetwayMainApplication {
    public static void main(String [] args){
        SpringApplication.run(GetwayMainApplication.class, args);
    }
}

 

 创建yml文件:resource ->New ->File

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
#80是默认端口,访问时可以不写。不如localhost/api/order 就是localhost:80/api/order
server:
  port: 80

 如上配置好服务端口、名字以及nocas注册中心地址。

重启gateway服务

如上,服务启动成功,由于不是普通的servlet编程,选择的是响应式编程,所以使用的网络框架是Netty

浏览器页面输入http://localhost:8848/nacos

如下,服务列表,找到已经启动并注册好的gateway服务了。

2)路由-规则配置

下面我们实现路由规则。有两个方式,1:配置 2:编程

1:配置方式实现路由

输入 application-route.yml  回车

建好后,在application.yml中修改下,包含这个文件:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
  profiles:
    include: route
#80是默认端口,访问时可以不写。不如localhost/api/order 就是localhost:80/api/order
server:
  port: 80

application-route.yml中代码如下:

spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**

上面routes里面是个数组,每个对象对应一个微服务的配置。

id:路由名字

uri:  lb代表loadbalancer负载均衡  service-order是微服务的名字

predicates是断言,也是个数据,可以配置请求的规则。上面的/api/order/**代表请求路径中包含/api/order的,就符合规则。 

 重启gateway服务和order服务,页面输入:http://localhost/api/order/readDb

503服务不可用。说明网关在调用时,没有发现这个服务。

去nacos看下: gateway和service-order都是有的。

 在看下负载均衡配置:

也是对的。 

这是因为新版springcloud将负载均衡单独抽取了出来,所以需要在gateway里面加入负载均衡依赖:

   <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

 

引入依赖,刷新后,重启服务,页面再次刷新:

可以看到,错误变为404。404一般是路径没有找到。 

http://localhost/api/order/readDb

网关在发送请求时,会找/api/order这样的路径。

但是在我们的order服务里面,并没有这个路径:

因此我们可以改下order服务的路径信息:

@RequestMapping("/api/order")
@Slf4j
@RestController
@RequestMapping("/api/order")
public class OrderController {
/*    @Value("${order.timeout}")
    private String timeOut;
    @Value("${order.auto-confirm}")
    private String aotuConfirm;*/
    @Autowired
    OrderService orderService;
    @Autowired
    OrderProperties orderProperties;
    //创建订单
    @GetMapping("/create")
    public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
        Order order = orderService.createOrder(productId,userId);
        return order;
    }
    //获取配置
    @GetMapping("/getConfig")
    public String getConfig(){
        //return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
        return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
                +"; dburl=" +orderProperties.getDbUrl();

    }
    //创建订单(秒杀)
    @GetMapping("/seckill")
    @SentinelResource(value = "seckill-order",fallback = "seckillFallback")
    public Order seckill(@RequestParam(value = "userId",required = false)Long userId,@RequestParam("productId")Long productId){

        Order order = orderService.createOrder(productId,userId);
        order.setId(Long.MAX_VALUE);
        return order;
    }
    public Order seckillFallback(Long userId, Long productId, Throwable e){
        log.info("seckillFallback.....");
        Order order = new Order();
        order.setId(productId);
        order.setUserId(userId);
        order.setAddress("异常信息:"+e.getClass());
        return order;
    }
    //读取请求
    @GetMapping("/readDb")
    public String readDb(){
        log.info("readDb...");

        return "readDb";
    }
    //写入请求
    @GetMapping("/writeDb")
    public String writeDb(){
        return "writeDb";
    }

}

同样修改下product服务的路径信息:

@RequestMapping("/api/product")

@RestController
@RequestMapping("/api/product")
public class ProductController {
    @Autowired
    ProductService productService;
    //查询商品
    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable("id")Long productId, HttpServletRequest httpServletRequest){

        String header = httpServletRequest.getHeader("X-Token");
        System.out.println("hello token=【"+header+"】");
       // int i = 10/0;
        Product product = productService.getById(productId);
 /*       try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }*/

        return product;
    }
}

注意因为order服务通过feign调用product,现在product路径发生变化,因此需要改下order里面的feign的代码:

 

@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
    @GetMapping("/api/product/product/{id}")//标注在controller里面是接收请求,标注在FeignClient上是发送请求

    Product getProductById(@PathVariable("id") Long id);

}

 下面我们重启服务(1个gateway,2个product,2个order)

 刷新页面四次http://localhost/api/order/readDb

可以看到,页面返回正常数据,且每个order服务都被调用了2次打印了2次readDb日志。

3)路由-工作原理

 当请求进入网关时,会根据路由规则匹配。

id:全局唯一,不重复 

URI:目的地,就是要转发去的地址

Predicates:断言规则。就是什么情况下转给目的地。因为它是数组,如果有多个断言规则,必须同时符合,才会转发。

Filter:过滤器。就是哪些路径,不转发,排除掉。

GatewayHandlerMapping,负责筛选匹配路由规则,知道哪些请求需要发到对应的哪些目的地。

GatewayWebHandler,处理器,调用filter,链路,把请求转下去。

当请求经过这2个组件发送给目的地后,目的地返回数据,又会经过filter,GatewayWebHandler,

GatewayHandlerMapping把数据返回。

注意:routes里面的配置,是从上到下依次匹配,如果请求先被上面的匹配到了,那么下面的即使也符合,但是也不会转发了。

我们验证下:

修改路由规则代码,最后面加上一个baidu

spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - Path=/**

重启服务,浏览器输入localhost 

浏览器输入 http://localhost/api/order/readDb

输入localhost,因为是默认80,所以相当于直接访问网关,网关配置里面,order和product都不符合,但是最后的 Path=/** 符合,所以就转给了baidu。

输入http://localhost/api/order/readDb,由于符合order的规则,所以调用了order打印了readDb。

下面如果我们把配置中的路由交换顺序:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - Path=/**
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**


重启服务,会发现,输入 http://localhost/api/order/readDb   会报404,而localhost,会转给baidu。

 因为第一个bing-route都符合,所以输入。localhost,会转到

https://www.baidu.com/

而输入  http://localhost/api/order/readDb ,会转到https://www.baidu.com/api/order/readDb,而baidu这个服务器里面并没有这个order接口,所以404了。

所以在配置的时候,最前面的会优先匹配,要把兜底的放最后面。

配置中有个order属性,代表排序,0最大,1其次,数值越大,优先级越低。

这个配置

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - Path=/**
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**

和这个配置:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - Path=/**
          order: 0
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2

 其实是一样的,因为默认就是从上往下,优先级越来越低。但是如果我们把第一个配置order改为10

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - Path=/**
          order: 10
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2

重启服务后,那么我们输入 http://localhost/api/order/readDb  因为这个地址和bing-route以及order-route都匹配,但是bing-route(order为10)优先级低,所以还是走order-route(order为1),因此这时候页面就会调通。

4)断言-长短写法

我们上面的断言,是属于长写法,下面我们把长写法注释,改为短写法:


spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - Path=/**
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2

 断言中,

name:断言名称 

patterns: 匹配规则

matchTrailingSlash: 如果为true,表示,如果规则是/order/{segment},/order/5/和/order/5是一样的都可以匹配上,如果是false,那么/order/5/ 就匹配不上了。就是最后一个/是否可以忽略差异。

重启服务,页面输入http://localhost/api/order/readDb

可以调通,说明配置是对的。 

5)断言-Query

断言有很多类型,如上图。 

我们上面用的是Path,即路径匹配。更多规则这里不再讲述。我们讲一个Query。

如上图Query,是要求包含指定参数。我们配置下如下yml

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2

yml中,我们配置了bing-route,访问baidu,Path配置的是/s,代表如果路径是/s,就符合规则。

Query,我们配置的是参数wd,值为haha。代表,入参需要是wd,而且wd的值为haha,就符合规则。

这样就是,Path和Query这2个条件,必须都满足,才会转发给baidu。

我们重启服务,输入 http://localhost/s?wd=haha

回车后,发现转给了baidu。

如果我们输入http://localhost/s?wd=hehe

就会报404.。因为不符合Query条件。

 说明:咱们正常在baidu搜索时,输入haha,可以看到访问地址,下图中可以看到,baidu在搜索时其实调用的是/s接口,wd的值是我们输入的搜索的值。我们上面的配置实际上就是调用了baidu的这个搜索接口。

6)断言-自定义断言工厂

有时候我们需要自定义断言规则。

假设我们需要判断,参数中需要有一个叫user,并且值是lbjwade。

创建断言类:

输入: predicate.VipRoutePredicateFactory

@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
    public VipRoutePredicateFactory(){super(Config.class);};

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("param", "value");
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate(){
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                ServerHttpRequest request =serverWebExchange.getRequest();
                String first = request.getQueryParams().getFirst(config.getParam());
                return StringUtils.hasText(first) && first.equals(config.getValue());
            }
        };
    }

    @Validated
    public static class Config {
        private @NotEmpty String param;
        private @NotEmpty String value;




        public @NotEmpty String getParam() {
            return this.param;
        }

        public void setParam(@NotEmpty String param){
            this.param = param;
        }
        public @NotEmpty String getValue() {
            return this.value;
        }

        public void setValue(@NotEmpty String value){
            this.value = value;
        }

    }
}

上面代码是仿照 QueryRoutePredicateFactory这个类的来写的。因为Query是已经做好的匹配规则。

 

注意代码中的两个参数param和value,实际就对应了yml文件中的param和value,做规则匹配时,就是把里面的user和lbjwade按顺序作为入参,user赋值给param,lbjwade赋值给value。 

上面这个就是做的规则校验,标注1的,意思是取出请求中的参数为config.getParam()对应的值。即user这个参数对应的值。标注2的,意思就是请求中user对的值不为空。 标注3,意思就是user对应的值等于lbjwade。这样就是如果user值不为空并且user的值是lbjwade,那么就匹配规则。

修改配置规则:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2

注意配置中的name的值Vip是需要跟类的名字对应的。VipRoutePredicateFactory。也就是说name的值拼接上RoutePredicateFactory,就是需要创建的类的名字。

我们重启服务,然后输入http://localhost/s?wd=haha&user=lbjwade

发现浏览器正常转给了baidu。如下:

 如果我们改下浏览器里面的入参user的值再次请求,发现404,说明断言生效了。

我们再把user改回正确的,但是把wd改为错误的,发现还是404。这就验证了,断言里面的每一个都要匹配成功,才会转发。 

7)过滤器-基本使用

请求进入时,会先进入前置过滤器进行处理,然后在去调用接口,接口返回后,再经过后置过滤器处理,然后返回数据。

同上面的断言一样,过滤器也有很多分类,实现不同的功能。下面我们介绍一种:

RewritePath GatewayFilter Factory 路径重写

前面我们做网关功能时,因为路径中有/api/order,网关会找对应的路径,为了解决这个问题,我们是把所有的接口都在代码里面加上了/api/order这个路径才访问通了。实际中,这样操作很麻烦,需要把所有的代码都修改一遍。但是利用这个路径重写,我们就可以不用重写了。

 我们先把/api/order都注释掉:

order服务的ordercontroller类如下

@Slf4j
@RestController
//@RequestMapping("/api/order")
public class OrderController {
/*    @Value("${order.timeout}")
    private String timeOut;
    @Value("${order.auto-confirm}")
    private String aotuConfirm;*/
    @Autowired
    OrderService orderService;
    @Autowired
    OrderProperties orderProperties;
    //创建订单
    @GetMapping("/create")
    public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
        Order order = orderService.createOrder(productId,userId);
        return order;
    }
    //获取配置
    @GetMapping("/getConfig")
    public String getConfig(){
        //return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
        return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
                +"; dburl=" +orderProperties.getDbUrl();

    }
    //创建订单(秒杀)
    @GetMapping("/seckill")
    @SentinelResource(value = "seckill-order",fallback = "seckillFallback")
    public Order seckill(@RequestParam(value = "userId",required = false)Long userId,@RequestParam("productId")Long productId){

        Order order = orderService.createOrder(productId,userId);
        order.setId(Long.MAX_VALUE);
        return order;
    }
    public Order seckillFallback(Long userId, Long productId, Throwable e){
        log.info("seckillFallback.....");
        Order order = new Order();
        order.setId(productId);
        order.setUserId(userId);
        order.setAddress("异常信息:"+e.getClass());
        return order;
    }
    //读取请求
    @GetMapping("/readDb")
    public String readDb(){
        log.info("readDb...");

        return "readDb";
    }
    //写入请求
    @GetMapping("/writeDb")
    public String writeDb(){
        return "writeDb";
    }

}

 order服务的ProductFeignClient修改如下:

@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
    //@GetMapping("/api/product/product/{id}")//标注在controller里面是接收请求,标注在FeignClient上是发送请求
    @GetMapping("/product/{id}")

    Product getProductById(@PathVariable("id") Long id);

}

product服务的ProductController如下:

@RestController
//@RequestMapping("/api/product")
public class ProductController {
    @Autowired
    ProductService productService;
    //查询商品
    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable("id")Long productId, HttpServletRequest httpServletRequest){

        String header = httpServletRequest.getHeader("X-Token");
        System.out.println("hello token=【"+header+"】");
       // int i = 10/0;
        Product product = productService.getById(productId);
 /*       try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }*/

        return product;
    }
}

 修改后,我们重启微服务,页面输入http://localhost/api/order/readDb

 可以看到是404原因就是网关在如下配置中匹配到了service-order微服务,就去转给service-order,访问localhost:8000/api/order/readDb。但是因为我们刚刚把代码中/api/order注释掉了。所以找不到了,就报了404

下面我们实现路径重写功能:

 很简单,修改下网关的配置即可:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:  # /api/order/a/bc   /a/bc
            - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*),/$\{segment}
          order: 2

我们在yml中配置了

RewritePath=/api/order/?(?<segment>.*),/$\{segment}

这段代码意思就是把/api/order开头的路径,去掉/api/order。

举例: /api/order/aa/bb 会被改为/aa/bb。

同理我们把/api/product的也做了配置。

具体的这段代码不做解释了(我也不太懂),可以问下豆包,或者deepseek,通义灵码。

重启gateway微服务,然后刷新页面,可以看到访问通了。

过滤器还有个类是AddResponseHeaderGatewayFilterFactory,可以实现响应头添加内容。

我们可以打开f12浏览器的开发者工具,然后刷新页面再调一下接口,然后选择Network->接口名字->Headers->Response Headers。可以看到有三行内容。

 我们在gateway的yml配置中添加过滤功能:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:  # /api/order/a/bc   /a/bc
            - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
            - AddResponseHeader=X-response-abc,123
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*),/$\{segment}
          order: 2

 其中我们在order微服务的过滤器中添加了

filters:  # /api/order/a/bc   /a/bc
  - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
  - AddResponseHeader=X-response-abc,123

这段代码, - AddResponseHeader=X-response-abc,123  的意思就是添加key为X-response-abc,值为123的响应头。

我们重启gateway微服务,然后刷新页面重新调用下接口,可以在f12 后台看到返回了这个响应头。

8)过滤器-默认filter

默认filter是对所有的微服务都起作用的。刚刚上面的添加响应头,我们只对order服务添加了,所以调用product服务,不会返回值为123的响应头。我们验证下:

浏览器输入:http://localhost/api/product/product/23

下面我们修改下过滤器配置,把order服务的过滤器注释掉,在外面放一个默认过滤器:

 

spring:
  cloud:
    gateway:

      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:  # /api/order/a/bc   /a/bc
            - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
            #- AddResponseHeader=X-response-abc,123
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*),/$\{segment}
          order: 2
      default-filters:
        - AddResponseHeader=X-response-abc,123

 重启gateway服务,页面输入:http://localhost/api/product/product/23

可以看到添加了响应头123

下面我们在页面输入order的接口: http://localhost/api/order/readDb

依然返回了123的响应头,虽然我们把order的响应123的配置注释了,但是我们添加了默认的,所以对order和product服务,都会生效。 

9)过滤器-GlobalFilter

全局过滤器

假设我们需要记录一个请求,从开始进来到返回数据,所有的时间,即响应时间。下面我们创建一个过滤器类来实现:

鼠标右键->New->Java Class

输入filter.RtGlobalFilter回车

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter , Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String uri = request.getURI().toString();
        long start = System.currentTimeMillis();
        log.info("请求{}开始,时间:{}",uri,start);
        //===================以上是前置逻辑======================
        Mono<Void> filter = chain.filter(exchange).doFinally(
                (result)->{
                    //==================以下是后置逻辑=============================
                    long end = System.currentTimeMillis();
                    log.info("请求{}结束,时间:{},耗时:{}",uri,end,end-start);


                }
        );//放行
        return filter;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

 重启gateway,页面输入http://localhost/api/order/readDb,回车

10)过滤器-自定义过滤器工厂

尽管springcloud提供了一些过滤器,但是有时候并不能完全满足需求,需要我们自己定义过滤器。

可以参照AddResponseHeaderGatewayFilterFactory这个类进行编写。

假如我们需要给order微服务编写一个过滤器,给它生成uuid类的token,或者jwt类的token。

先配置yml如下:

spring:
  cloud:
    gateway:

      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:  # /api/order/a/bc   /a/bc
            - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
            - OnceToken=X-Response-Token,uuid
            #- AddResponseHeader=X-response-abc,123
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*),/$\{segment}
          order: 2
      default-filters:
        - AddResponseHeader=X-response-abc,123

 

双击shift,输入AddResponseHeaderGatewayFilterFactory,选择第一个,打开

如上图,可以参照这个来编写。 AddResponseHeaderGatewayFilterFactory这个类,后缀是GatewayFilterFactory,我们可以写一个类名字里面加上这个后缀。

如上图,filter包,鼠标右键->New->Java Class

输入 OnceTokenGatewayFilterFactory,回车

import org.apache.http.client.methods.HttpHead;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter(){

            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                return chain.filter(exchange).then(Mono.fromRunnable(()->{
                    OnceTokenGatewayFilterFactory.this.onceToken(exchange,config);
                }));
            }
        };
    }

    private void onceToken(ServerWebExchange exchange, NameValueConfig config) {
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders httpHeaders = response.getHeaders();
        String value = config.getValue();
        if("uuid".equalsIgnoreCase(value)){
            value = UUID.randomUUID().toString();
        }
        if("jwt".equalsIgnoreCase(value)){
            //此处可以通过豆包或者其他ai软件让他们生成一个jwt数据示例
            value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlSXNzdWVyIiwic3ViIjoidXNlcjEyMyIsImF1ZCI6Im15YXBwIiwiZXhwIjoxNjg2NDIyNDAwLCJpYXQiOjE2ODY0MTg4MDAsInJvbGUiOiJhZG1pbiJ9.abcdefghijklmnopqrstuvwxyz0123456789";
        }
        httpHeaders.add(config.getName(),value);
    }
}

 我们重启下服务,浏览器输入http://localhost/api/order/readDb

如下图,可以看到,带上了一个uuid类型的token

 

我们把yml中配置改为jwt的token,如下:

spring:
  cloud:
    gateway:

      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:  # /api/order/a/bc   /a/bc
            - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
            - OnceToken=X-Response-Token,jwt
            #- AddResponseHeader=X-response-abc,123
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*),/$\{segment}
          order: 2
      default-filters:
        - AddResponseHeader=X-response-abc,123

 重启服务后,输入http://localhost/api/order/readDb 回车

如上图,可以看到返回了一个jwt的token(其实这个token代码中是写死了,应该写一个每次都生成token的方法,可以找ai工具生成一下) 

11)全局跨域

       当项目不是微服务架构,只有一个单体项目时,如果要解决跨域问题,可以在controller上面加一个注解@CrossOrigin。当这个项目controller类很多时,每个都加这个注解,比较麻烦,这时候,可以写一个全局的filter解决这个跨域问题。但是,如果是微服务项目,如果每个微服务都写一个filter解决跨域,那就很麻烦了。因为本身网关就是统一入口,因此可以在网关上入手,统一解决跨域问题。

我们先看下,没有添加全局跨域时,页面输入http://localhost/api/order/readDb

可以看到响应里面没有跨域内容。

下面我们添加跨域配置:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: '*'
            allowed-headers: '*'
            allowed-methods: '*'

      routes:
        - id: bing-route
          uri: https://www.baidu.com/
          predicates:
            - name: Path
              args:
                patterns: /s
            - name: Query
              args:
                param: wd
                regexp: haha
            - name: Vip
              args:
                param: user
                value: lbjwade
          order: 10
        - id: order-route
          uri: lb://service-order
#          predicates:
#            - Path=/api/order/**
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:  # /api/order/a/bc   /a/bc
            - RewritePath=/api/order/?(?<segment>.*),/$\{segment}
            - OnceToken=X-Response-Token,jwt
            #- AddResponseHeader=X-response-abc,123
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*),/$\{segment}
          order: 2
      default-filters:
        - AddResponseHeader=X-response-abc,123

 

 如上图,就是跨域配置

接下来我们页面输入http://localhost/api/order/readDb回车

 对比两个响应头,可以看到多了3个允许跨域的相关字段

12)总结

面试题:微服务之间的调用,经过网关吗?

先说结论:技术上可以实现微服务之间调用网关,但是不推荐,微服务之间直接调用即可。网关主要是作为统一入口解决前端和后端微服务之间的调用而产生的。微服务之间使用网关,浪费资源,没有多大作用。

验证:

我们页面上输入http://localhost:8000/create?userId=5&productId=88

回车后,看到页面返回了数据。我们去gateway控制台,看到,并没有打印任何日志。

如下图所示。

下面我们可以改下代码,使其经过网关。由于创建订单接口是order服务调用的product,所以在order服务的feign里面改下路径:

import com.atguigu.order.feign.fallbak.ProductFeignClientFallback;
import com.atguigu.product.bean.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class)

@FeignClient(value = "gateway",fallback = ProductFeignClientFallback.class)//经过网关的配置
public interface ProductFeignClient {
    //@GetMapping("/api/product/product/{id}")//标注在controller里面是接收请求,标注在FeignClient上是发送请求
    //@GetMapping("/product/{id}")
    @GetMapping("/api/product/product/{id}") //经过网关的配置

    Product getProductById(@PathVariable("id") Long id);

}

重启order服务,我们刷新下页面,可以看到如下图,正常返回了数据,而且gateway打印了日志,order也打印了访问路径,说明走的是网关。

 

2.Seata

分布式事务产生的原因:

如上图,左侧是传统的单体项目的事务,在一个连接中,如果发生异常,会回滚,保证业务逻辑的健壮性。但是右侧的分布式微服务当中,每个微服务都对应自己的数据库,在一个场景中,需要创建订单,削减库存,用户账户积分,三个操作都进行。假设用户积分失败了,那么需要创建订单和削减库存也回滚下。但是在分布式微服务当中,每个连接都是单独的,没法回滚,这时候就需要分布式事务来解决这个问题。

1)环境搭建

如上图,一共4个微服务:采购、库存、订单、账户。采购微服务有个采购接口,里面是用户完成采购逻辑。采购逻辑里面,一方面需要调用库存服务扣库存,一方面需要调用订单服务下订单。而订单服务下订单的逻辑中,需要调用账户服务,把余额减掉这次购买的金额。前面已经介绍了如何在项目中创建services模块下的子模块service-order和service-product两个微服务,可以参考下,继续创建seata-order、seata-storage、seata-business、seata-account四个微服务。

我们简单说下创建过程,详细的可以看前面的springcloud笔记哈。

 鼠标右键 services->New->Module

 如上图,填入seata-business,java Maven  JDK17,parent一定要选services,然后点击创建。

如上创建成功。

按上面方法,把四个微服务都创建好。 

下面,我们可以下载一个数据库管理软件,dbeaver或者navicat都行。

然后安装mysql。可以搜下教程哈。

安装好mysql后,本地运行mysql。

然后打开dbeaver工具,在本地数据库创建3个数据库:account_tbl  order_tbl  storage_tbl。

 然后,打开account_tbl数据库,运行如下建表语句:

CREATE TABLE `account_tbl` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int DEFAULT NULL COMMENT '用户id',
  `money` int DEFAULT NULL COMMENT '存款',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

然后在account_tbl中插入如下数据:

INSERT INTO account_tbl.account_tbl (user_id,money) VALUES 
(1,10000)
;

同理,在order_tbl数据库,运行如下建表语句:

CREATE TABLE `order_tbl` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `commodity_code` varchar(100) DEFAULT NULL,
  `count` int DEFAULT NULL,
  `money` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

 在storage_tbl数据库,运行如下建表语句:

CREATE TABLE `storage_tbl` (
  `id` int NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(100) DEFAULT NULL,
  `count` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

然后在storage_tbl中插入如下2条数据:

INSERT INTO storage_tbl.storage_tbl (commodity_code,count) VALUES 
('P0001',100)
,('B1234',10)
;

 然后我们在services的pom中添加如下依赖:

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.31</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 手动添加兼容版本 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.16</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>

因为services是父项目,在这里添加依赖会使每一个微服务自动引入。

 然后我们在seata-account的resources里面创建application.yml文件,并添加以下内容:

spring:
  application:
    name: seata-account
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/account_tbl?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: xxxx
    password: yyyy
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      config:
        import-check:
          enabled: false
server:
  port: 10000
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true  # 驼峰映射(建议开启)

 这是配置的数据库连接以及使用mybatis-plus来进行数据库的操作。里面记得把username和password换成自己的数据库的用户名和密码。

接下来就是创建微服务的java代码了。就是controller service  等等。可以手动创建,还有个便捷的方式就是使用插件,下面简单介绍下。

先下载一个MyBatisX,安装好。

 然后打开刚刚的application.yml文件,右侧选择Database,点击+,

然后选择import data sources

 在上图的弹出来的框框填入账号和密码,点击 Test Connection

看到successed,然后点击ok

 

如上图可以看到已经出现account_tbl数据库了。 

鼠标右键表 account_tbl,然后点击MybatisX-Generator。下面的工作就是用这个插件,自动生成这个表的各种类,比如实体类bean,service,mapper,serviceimpl等。它会自动根据表中的字段生成这部分代码。很方便实用。

在module path里面输入seata-account的路径D:/softs/cloud-demo/services/seata-account(根据自己的实际的来写),base package填入:com.atguigu.account 点击下一步 

 勾选上图中的5个选项,点击完成。

如上图,可以看到自动生成了这些代码,是不是很方便呢。

 下面提供下自动生成的代码:

@Mapper
public interface AccountTblMapper extends BaseMapper<AccountTbl> {

}

上面的@Mapper好像没有自动生成,手动添加上。

@TableName(value ="account_tbl")
@Data
public class AccountTbl implements Serializable {
    /**
     * 主键
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 用户id
     */
    private Integer userId;

    /**
     * 存款
     */
    private Integer money;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}
@Service
public class AccountTblServiceImpl extends ServiceImpl<AccountTblMapper, AccountTbl>
    implements AccountTblService{

}


public interface AccountTblService extends IService<AccountTbl> {

}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.account.mapper.AccountTblMapper">

    <resultMap id="BaseResultMap" type="com.atguigu.account.order.AccountTbl">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="userId" column="user_id" jdbcType="INTEGER"/>
            <result property="money" column="money" jdbcType="INTEGER"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,user_id,money
    </sql>
</mapper>

接下来我们写一下controller。

 输入controller.AccountRestController  回车

@RestController
public class AccountRestController {
    @Autowired
    AccountTblService accountTblService;
    @GetMapping("/debit")
    public String debit(@RequestParam("userId") int userId,
                        @RequestParam("money") int money){
        accountTblService.debit(userId,money);
        return "account debit success";

    }
}

@Mapper
public interface AccountTblMapper extends BaseMapper<AccountTbl> {

    void debit(int userId, int money);
}


@Service
public class AccountTblServiceImpl extends ServiceImpl<AccountTblMapper, AccountTbl>
    implements AccountTblService{
    @Autowired
    AccountTblMapper accountTblMapper;

    @Override
    public void debit(int userId, int money) {
        accountTblMapper.debit(userId,money);

    }
}
public interface AccountTblService extends IService<AccountTbl> {

    void debit(int userId, int money);
}
@SpringBootApplication
@EnableFeignClients
@MapperScan("com.atguigu.account.mapper")
public class AccountMainApplication {
    public static void main(String [] args){
        SpringApplication.run(AccountMainApplication.class,args);
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.account.mapper.AccountTblMapper">

    <resultMap id="BaseResultMap" type="com.atguigu.account.order.AccountTbl">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="userId" column="user_id" jdbcType="INTEGER"/>
            <result property="money" column="money" jdbcType="INTEGER"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,user_id,money
    </sql>
    <update id="debit">
        update account_tbl set money = money- #{money}
        where user_id = #{userId}
    </update>
</mapper>

 上面是完整代码。

然后application.yml如下:

spring:
  profiles:
    include: feign
    active: dev
  application:
    name: seata-account
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/account_tbl?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: xxxx
    password: yyyy
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      config:
        import-check:
          enabled: false
        namespace: ${spring.profiles.active:dev}
server:
  port: 10000
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true  # 驼峰映射(建议开启)
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: dev
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: test
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: prod

 注意上面的数据库账号和密码需要修改。

application-feign.yml如下:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
          seata-account:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true
      web-context-unify: false
feign:
  sentinel:
    enabled: true

pom中引入如下依赖:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

 我们重启服务,然后使用apipost调用接口http://localhost:10000/debit?userId=1&money=9

可以看到接口返回成功,而且数据库中用户1金额会减少9(从10000变为9991) 

如上account服务已经搭建好,并且测试成功,接口正常。

 下面我们在根据order_tbl表生成seata-order服务的代码。也是用mybatis插件直接生成,当然也可以自己写哈。

下面直接贴代码了:

package com.atguigu.seataorder.bean;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;

/**
 * 
 * @TableName order_tbl
 */
@TableName(value ="order_tbl")
@Data
public class OrderTbl implements Serializable {
    /**
     * 
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 
     */
    private Integer userId;

    /**
     * 
     */
    private String commodityCode;

    /**
     * 
     */
    private Integer count;

    /**
     * 
     */
    private Integer money;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}
@RestController
public class OrderRestController {
    @Autowired
    OrderTblService orderTblService;
    @GetMapping("/create")
    public String create(@RequestParam("userId")int userId,
                         @RequestParam("commodityCode")String commodityCode,
    @RequestParam("count")int count){
        OrderTbl tbl = orderTblService.create(userId,commodityCode,count);
        return "order create success 订单id:"+tbl.getId();

    }
}
@Mapper
public interface OrderTblMapper extends BaseMapper<OrderTbl> {

}

@Service
public class OrderTblServiceImpl extends ServiceImpl<OrderTblMapper, OrderTbl>
    implements OrderTblService{
    @Autowired

    OrderTblMapper orderTblMapper;

    @Override
    public OrderTbl create(int userId, String commodityCode, int count) {
        //计算价格:
        int orderMoney = calcuate(commodityCode,count);
        //todo 扣减余额
        //保存订单:
        OrderTbl orderTbl = new OrderTbl();
        orderTbl.setUserId(userId);
        orderTbl.setCommodityCode(commodityCode);
        orderTbl.setCount(count);
        orderTbl.setMoney(orderMoney);
        orderTblMapper.insert(orderTbl);


        return orderTbl;
    }

    private int calcuate(String commodityCode, int count) {
        return 9*count;
    }
}

public interface OrderTblService extends IService<OrderTbl> {

    OrderTbl create(int userId, String commodityCode, int count);
}
@EnableFeignClients
@SpringBootApplication
@MapperScan("com.atguigu.seataorder.mapper")
public class SeataOrderMainApplication {
    public static void main(String[] args){
        SpringApplication.run(SeataOrderMainApplication.class,args);

    }
}
   <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    </dependencies>

 application.yml如下:(注意修改数据库账号和密码)

spring:
  profiles:
    active: dev
  application:
    name: seata-order
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_tbl?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: xxxx
    password: yyyy
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      config:
        import-check:
          enabled: false
        namespace: ${spring.profiles.active:dev}
server:
  port: 12000
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true  # 驼峰映射(建议开启)
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: dev
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: test
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: prod

 application-feign.yml如下:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
          service-product:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true
      web-context-unify: false
feign:
  sentinel:
    enabled: true

 我们启动微服务,然后利用apipost调用接口:http://localhost:12000/create?userId=1&count=2&commodityCode=P0001

 

 如上可以看到,接口返回成功,数据库中也生成了订单了。

接下来我们完善库存微服务:

直接上代码:

@RestController
public class StorageController {
    @Autowired
    StorageTblService storageTblService;
    @GetMapping("/deduct")
    public String  deduct(@RequestParam("commodityCode")String commodityCode,
                          @RequestParam("count")Integer count){
        storageTblService.deduct(commodityCode,count);
        return "storage success";

    }
}
@Mapper
public interface StorageTblMapper extends BaseMapper<StorageTbl> {

    void deduct(String commodityCode, Integer count);
}


@TableName(value ="storage_tbl")
@Data
public class StorageTbl implements Serializable {
    /**
     * 
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 
     */
    private String commodityCode;

    /**
     * 
     */
    private Integer count;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

@Service
public class StorageTblServiceImpl extends ServiceImpl<StorageTblMapper, StorageTbl>
    implements StorageTblService{
    @Autowired
    StorageTblMapper storageTblMapper;

    @Override
    public void deduct(String commodityCode, Integer count) {
        storageTblMapper.deduct(commodityCode,count);

    }
}
public interface StorageTblService extends IService<StorageTbl> {

    void deduct(String commodityCode, Integer count);
}

@EnableFeignClients
@SpringBootApplication
@MapperScan("com.atguigu.storage.mapper")
public class StorageMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(StorageMainApplication.class,args);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.storage.mapper.StorageTblMapper">

    <resultMap id="BaseResultMap" type="com.atguigu.storage.order.StorageTbl">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="commodityCode" column="commodity_code" jdbcType="VARCHAR"/>
            <result property="count" column="count" jdbcType="INTEGER"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,commodity_code,count
    </sql>
    <update id="deduct">
        update storage_tbl set count = count - #{count}
        where commodity_code = #{commodityCode}
    </update>
</mapper>

 application.yml  (注意了这里面数据库的账号和密码改为自己的哈)

spring:
  profiles:
    active: dev
  application:
    name: seata-storage
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/storage_tbl?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: xxxx
    password: yyyy
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      config:
        import-check:
          enabled: false
        namespace: ${spring.profiles.active:dev}
server:
  port: 13000
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true  # 驼峰映射(建议开启)
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: dev
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: test
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: prod

 application-feign.yml

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
          service-product:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true
      web-context-unify: false
feign:
  sentinel:
    enabled: true

 重启服务,然后在apipost里面请求http://localhost:13000/deduct?count=2&commodityCode=P0001

 

可以看到接口返回成功,并且P0001的库存变成了98。

下面我们完善seata-business服务。下面直接上代码:

@RestController
public class BusinessController {
    @Autowired
    BusinessService businessService;
    @GetMapping("/purchase")
    public String purchase(@RequestParam("userId")Integer userId,
                           @RequestParam("commodityCode")String commodityCode,
                           @RequestParam("count")Integer count){
        businessService.purchase(userId,commodityCode,count);

        return "business success";
    }
}
@Service
public class BusinessServiceImpl implements BusinessService {
    @Override
    public void purchase(Integer userId, String commodityCode, Integer count) {
        //todo 削减库存
        //todo 创建订单

    }
}
public interface BusinessService {
    void purchase(Integer userId, String commodityCode, Integer count);
}
@EnableFeignClients
@SpringBootApplication
public class BusinessMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(BusinessMainApplication.class,args);
    }
}

 application.yml(注意修改下数据库账号和密码)

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_tbl?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: lbj12345
  profiles:
    active: dev
  application:
    name: seata-business
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      config:
        import-check:
          enabled: false
        namespace: ${spring.profiles.active:dev}
server:
  port: 11000
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: dev
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: test
---
spring:
  config:
    import:
      - nacos:common.properties?group=order
      - nacos:database.properties?group=order
    activate:
      on-profile: prod

 application-feign.yml

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
          service-product:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true
      web-context-unify: false
feign:
  sentinel:
    enabled: true

2)接口测试

上面的4个服务,我们3个服务测试接口都是好用的。

3)本地事务测试

如果只是单个服务自己里面的事务,就是本地事务。

我们开启下storage服务的本地事务:

@EnableFeignClients
@SpringBootApplication
@MapperScan("com.atguigu.storage.mapper")
@EnableTransactionManagement//开启事务
public class StorageMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(StorageMainApplication.class,args);
    }
}

@Service
public class StorageTblServiceImpl extends ServiceImpl<StorageTblMapper, StorageTbl>
    implements StorageTblService{
    @Autowired
    StorageTblMapper storageTblMapper;

    @Override
    @Transactional//开启方法的事务。
    public void deduct(String commodityCode, Integer count) {
        storageTblMapper.deduct(commodityCode,count);
        //当数量为5时,主动抛出异常
        if(count==5){
            throw new RuntimeException("扣减失败");
        }

    }
}

当数量是5时,我们抛一个异常。当没有开启事务时,库存扣减后,即便抛异常,数量也会是扣减之后的。但是如果开启了事务,抛异常后,数量会变成原来的(即回滚)。

我们重启服务, 调用http://localhost:13000/deduct?count=2&commodityCode=P0001

发现返回成功,且库存减2。

但是当把数量改为5时,因为抛出异常了,所以库存回滚(不变) 

这就是本地事务的用法。

下面我们把order服务、account服务都加上本地事务注解,以便完善代码。

@EnableFeignClients
@SpringBootApplication
@MapperScan("com.atguigu.seataorder.mapper")
@EnableTransactionManagement
public class SeataOrderMainApplication {
    public static void main(String[] args){
        SpringApplication.run(SeataOrderMainApplication.class,args);

    }
}

@Service
public class OrderTblServiceImpl extends ServiceImpl<OrderTblMapper, OrderTbl>
    implements OrderTblService{
    @Autowired

    OrderTblMapper orderTblMapper;

    @Override
    @Transactional
    public OrderTbl create(int userId, String commodityCode, int count) {
        //计算价格:
        int orderMoney = calcuate(commodityCode,count);
        //todo 扣减余额
        //保存订单:
        OrderTbl orderTbl = new OrderTbl();
        orderTbl.setUserId(userId);
        orderTbl.setCommodityCode(commodityCode);
        orderTbl.setCount(count);
        orderTbl.setMoney(orderMoney);
        orderTblMapper.insert(orderTbl);


        return orderTbl;
    }

    private int calcuate(String commodityCode, int count) {
        return 9*count;
    }
}

@SpringBootApplication
@EnableFeignClients
@MapperScan("com.atguigu.account.mapper")
@EnableTransactionManagement
public class AccountMainApplication {
    public static void main(String [] args){
        SpringApplication.run(AccountMainApplication.class,args);
    }
}

@Service
public class AccountTblServiceImpl extends ServiceImpl<AccountTblMapper, AccountTbl>
    implements AccountTblService{
    @Autowired
    AccountTblMapper accountTblMapper;

    @Override
    @Transactional
    public void debit(int userId, int money) {
        accountTblMapper.debit(userId,money);

    }
}

4)打通远程链路

上面的本地事务,运行起来是没有问题的,但是如果是微服务之间的调用,就会有问题

我们先完善代码,使微服务之间可以正常调用

先完善business服务:

先在feign包下创建一个StorageFeignClient接口

@FeignClient(value = "seata-storage")
public interface StorageFeignClient {
    @GetMapping("/deduct")
    public String  deduct(@RequestParam("commodityCode")String commodityCode,
                          @RequestParam("count")Integer count);
}
@FeignClient(value = "seata-order")
public interface OrderFeignClient {
    @GetMapping("/create")
     String create(@RequestParam("userId")int userId,
                         @RequestParam("commodityCode")String commodityCode,
                         @RequestParam("count")int count);
}
@SpringBootApplication
@EnableFeignClients(basePackages = "com.atguigu.business.feign")
@EnableDiscoveryClient
public class BusinessMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(BusinessMainApplication.class,args);
    }
}
@Service
public class BusinessServiceImpl implements BusinessService {
    @Autowired
    StorageFeignClient storageFeignClient;
    @Autowired
    OrderFeignClient orderFeignClient;
    @Override
    public void purchase(Integer userId, String commodityCode, Integer count) {
        //削减库存
        storageFeignClient.deduct(commodityCode,count);
        //创建订单
        orderFeignClient.create(userId,commodityCode,count);


    }
}

 接下来完善order服务:

@FeignClient(value = "seata-account")
public interface AccountFeignClient {
    @GetMapping("/debit")
    public String debit(@RequestParam("userId") int userId,
                        @RequestParam("money") int money);
}

@Service
public class OrderTblServiceImpl extends ServiceImpl<OrderTblMapper, OrderTbl>
    implements OrderTblService{
    @Autowired
    AccountFeignClient accountFeignClient;

    OrderTblMapper orderTblMapper;

    @Override
    @Transactional
    public OrderTbl create(int userId, String commodityCode, int count) {
        //计算价格:
        int orderMoney = calcuate(commodityCode,count);
        //扣减余额
        accountFeignClient.debit(userId,orderMoney);
        //保存订单:
        OrderTbl orderTbl = new OrderTbl();
        orderTbl.setUserId(userId);
        orderTbl.setCommodityCode(commodityCode);
        orderTbl.setCount(count);
        orderTbl.setMoney(orderMoney);
        orderTblMapper.insert(orderTbl);
        int i = 20/0;//主动抛出异常。


        return orderTbl;
    }

    private int calcuate(String commodityCode, int count) {
        return 9*count;
    }
}

 我们在上面代码中故意在最后抛出一个异常。

@EnableFeignClients(basePackages = "com.atguigu.seataorder.feign")
@SpringBootApplication
@MapperScan("com.atguigu.seataorder.mapper")
@EnableTransactionManagement
@EnableDiscoveryClient
public class SeataOrderMainApplication {
    public static void main(String[] args){
        SpringApplication.run(SeataOrderMainApplication.class,args);

    }
}

 我们在order服务中最后抛出异常,那么订单服务因为有本地事务,所以订单不会生成数据,但是库存和账户两个因为是远程feign调用,无法因为订单抛出异常而回滚,就会导致,订单服务没有生成订单数据,但是库存和账户金额都改变了。我们验证下这点:

如上图,我们先把数据库数据改下,库存改为100,账户余额改为10000,订单表数据清空。

接下来调用 http://localhost:11000/purchase?userId=1&count=2&commodityCode=P0001

可以看到报错了。然后看下数据库数据如下:

 

可以看到,订单表确实回滚了(无订单生成),但是库存少了,金额也少了。

这种情况,就需要用到分布式事务了。 

5)架构原理

 TC:事务协调者,相当于一个统筹所有事务的角色。

TM:定义全局事务的范围。

RM:每一个分支事务。需要与TC进行通信,听从TC处理。

如果分支事务RM出错,报告给TC后,TC会要求所有的分支事务进行回滚,从而实现全局事务管理。

6)整合seata完成

下载seata:Seata Java Download | Apache Seata

选择后缀为bin.tar.gz的文件,下载后解压

 进入bin目录,搜索框输入cmd,然后回车

 在cmd窗口输入 seata-server.bat,回车,即可运行seata

 可以看到一个8091端口,是使用端口,7091是页面端口。在浏览器输入http://127.0.0.1:7091/

用户名和密码都输入seata。

登录后,点击右上角的中文,切换下语言:

在根目录的cloud-demo的pom文件中找到:

 ctrl+鼠标左键单击spring-cloud-alibaba-dependencies

然后在打开的页面输入seata

可以看到seata版本是2.1.0。所以刚才下载的时候,我们需要下载2.1.0版本的。 

再往下找到seata依赖

把这个依赖添加到services的pom中:

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2023.0.3.2</version>
      </dependency>

 

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 如上,这个建表语句在三个数据库中都运行,每个数据库都需要建立这个表。

 下面我们需要在每个微服务的resources里面,创建一个文件:file.conf

service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}

然后重启4个服务:order,business,storage,account。

在调用接口前,先把数据库数据刷一下,方便观察:

1.账户余额改为10000 2.订单表数据清空 3.库存改为100

接下来我们调用http://localhost:11000/purchase?userId=1&count=2&commodityCode=P0001

如上图,发现订单回滚了(本地事务起作用),但是库存和金额都减少了,分布式事务没有发挥作用。 

这是为什么呢?原因在于,没有注明全局事务的范围。

我们在开始的business服务的方法上添加注解@GlobalTransactional

@Service
public class BusinessServiceImpl implements BusinessService {
    @Autowired
    StorageFeignClient storageFeignClient;
    @Autowired
    OrderFeignClient orderFeignClient;
    @GlobalTransactional
    @Override
    public void purchase(Integer userId, String commodityCode, Integer count) {
        //削减库存
        storageFeignClient.deduct(commodityCode,count);
        //创建订单
        orderFeignClient.create(userId,commodityCode,count);


    }
}

 重启微服务,同时把数据库的数据刷新成10000余额,和100库存。

然后再次调用接口:

发现订单、余额、库存都回滚了。全局事务生效。

7)二阶提交协议流程

第一阶段:当全局事务开始时,每个分支事务,会查询数据修改前和修改后的数据,并保存信息到undo日志表中,方便回滚时恢复数据。在这个时候需要申请全局锁,防止外界有人改这个记录。

第二阶段:分支事务把结果通知TC,如果有任何一个分支事务报错回滚了,那么TC就会通知所有的分支事务都回滚,否则就不回滚,直接提交。同时异步删除undo log记录。

8)二阶提交可视化

debug模式启动项目,同时打断点如上:

然后调用business接口

http://localhost:11000/purchase?userId=1&count=2&commodityCode=P0001

此时刷新seata页面,可以看到事务信息,包括全局事务id,当前事务id,服务器名字,事务分组,业务入口,状态,超时时间(60s,指的是超过60秒自动全局回滚。) 

此时还没有进入分支事务,所以没有锁信息。

我们把第一个断点关掉,同时在下面打上断点,然后重新调用接口:

此时,因为库存服务已经进入了,所以会有锁,而且库存数据库undo表会有数据:

9)四种事务模式

SEATA AT模式:推荐用的方式。

SEATA XA模式:不推荐,性能低。(一开始就加锁,性能低,AT模式是第一阶段结束时加锁。)

SEATA TCC模式: 手动写的协议。

prepare:第一阶段的操作

commit:事务提交后的操作

rollback:事务回滚后的代码。

由于可以手动写逻辑,可以写一些数据库之外的回滚逻辑,比如一开始发一个邮件,如果需要回滚的话,但是邮件是已经发出去了,无法撤销,那就再发一封邮件,说明下第一个邮件已取消。

这就是广义上的事务。狭义上的事务是数据库模式。 

SEATA SAGA模式:适合时间延续很长的业务。配合消息队列使用。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐