1 服务注册与发现中心:Eureka

1.1 服务调用

01.图书管理系统项目
    a.拆分
        项目拆分一定要尽可能保证单一职责,相同的业务不要在多个微服务中重复出现,
        如果出现需要借助其他业务完成的服务,那么可以使用服务之间相互调用的形式来实现
    b.服务
        登录验证服务:用于处理用户注册、登录、密码重置等,反正就是一切与账户相关的内容,包括用户信息获取等。
        图书管理服务:用于进行图书添加、删除、更新等操作,图书管理相关的服务,包括图书的存储等和信息获取。
        图书借阅服务:交互性比较强的服务,需要和登陆验证服务和图书管理服务进行交互。

02.服务
    a.服务
        UserApplication         http://127.0.0.1:8101/user/{uid}
        BookApplication         http://127.0.0.1:8201/book/{bid}
        BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
    b.数据
        http://127.0.0.1:8101/user/1
        {
            "uid": 1,
            "name": "小明",
            "sex": "男"
        }
        -----------------------------------------------------------------------------------------------------
        http://127.0.0.1:8201/book/1
        {
            "bid": 1,
            "title": "深入了解Java虚拟机",
            "desc": "了解Java的底层运作机制"
        }
        -----------------------------------------------------------------------------------------------------
        http://127.0.0.1:8301/borrow/1
        {
            "user": {
                "uid": 1,
                "name": "小明",
                "sex": "男"
            },
            "bookList": [
                {
                    "bid": 1,
                    "title": "深入了解Java虚拟机",
                    "desc": "了解Java的底层运作机制"
                },
                {
                    "bid": 2,
                    "title": "Java并发编程的艺术",
                    "desc": "了解并发编程的高级玩法"
                }
            ]
        }

03.图书借阅服务
    a.Service接口
        public interface BorrowService {
            UserBorrowDetail getUserBorrowDetailByUid(int uid);
        }
    b.Service实现
        @Service
        public class BorrowServiceImpl implements BorrowService{
        
            @Resource
            BorrowMapper mapper;
        
            @Override
            public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
                List<Borrow> borrow = mapper.getBorrowsByUid(uid);
                //那么问题来了,现在拿到借阅关联信息了,怎么调用其他服务获取信息呢?
            }
        }

        需要进行服务远程调用我们需要用到`RestTemplate`来进行:

        @Service
        public class BorrowServiceImpl implements BorrowService{

            @Resource
            BorrowMapper mapper;

            @Override
            public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
                List<Borrow> borrow = mapper.getBorrowsByUid(uid);
                //RestTemplate支持多种方式的远程调用
                RestTemplate template = new RestTemplate();
                //这里通过调用getForObject来请求其他服务,并将结果自动进行封装
                //获取User信息
                User user = template.getForObject("http://localhost:8082/user/"+uid, User.class);
                //获取每一本书的详细信息
                List<Book> bookList = borrow
                        .stream()
                        .map(b -> template.getForObject("http://localhost:8080/book/"+b.getBid(), Book.class))
                        .collect(Collectors.toList());
                return new UserBorrowDetail(user, bookList);
            }
        }
    c.Controller
        @RestController
        public class BorrowController {

            @Resource
            BorrowService service;

            @RequestMapping("/borrow/{uid}")
            UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
                return service.getUserBorrowDetailByUid(uid);
            }
        }

1.2 服务注册与发现

01.服务注册
    a.配置
        场景:eureka-server(服务注册与发现中心) -> user-server、book-server、borrow-server(注册的三个微服务)
        -----------------------------------------------------------------------------------------------------
        一、服务端(eureka-server)
        1.pom.xml(依赖)
        <!--Eureka服户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        2.application.yml(配置)
        server:
          port: 8888

        eureka:
          # 开启之前需要修改一下客户端设置(虽然是服务端)
          client:
            # 由于我们是作为服务端角色,所以不需要获取服务端,改为false,默认为true
            fetch-registry: false
            # 暂时不需要将自己也注册到Eureka
            register-with-eureka: false
            # 将eureka服务端指向自己
            service-url:
              defaultZone: http://localhost:8888/eureka/

        3.启动类(编码)
        @EnableEurekaServer//启动服务端
        @SpringBootApplication
        public class EurekaServerApplication {

            public static void main(String[] args) {
                SpringApplication.run(EurekaServerApplication.class);
            }
        }
        -------------------------------------------------------------------------------------------------------------

        二、客户端(user-server)
        1.pom.xml(依赖)
        <!--Eureka服户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        2.application.yml(配置)
        server:
          port: 8101

        spring:
          application:
            name: userservice

        eureka:
          client:
            # 跟上面一样,需要指向Eureka服务端地址,这样才能进行注册
            service-url:
              defaultZone: http://localhost:8888/eureka/

        3.启动类(编码)
        @EnableEurekaClient//开启客户端
        @SpringBootApplication
        public class UserApplication {
            public static void main(String[] args) {
                SpringApplication.run(UserApplication.class, args);
            }
        }

        -------------------------------------------------------------------------------------------------------------

        三、客户端(book-server)
        1.pom.xml(依赖)
        <!--Eureka服户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        2.application.yml(配置)
        server:
          port: 8201

        spring:
          application:
            name: bookservice

        eureka:
          client:
            # 跟上面一样,需要指向Eureka服务端地址,这样才能进行注册
            service-url:
              defaultZone: http://localhost:8888/eureka/

        3.启动类(编码)
        @EnableEurekaClient//开启客户端
        @SpringBootApplication
        public class BookApplication {
            public static void main(String[] args) {
                SpringApplication.run(BookApplication.class, args);
            }
        }

        -------------------------------------------------------------------------------------------------------------

        四、客户端(borrow-server)
        1.pom.xml(依赖)
        <!--Eureka服户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        2.application.yml(配置)
        server:
          port: 8301

        spring:
          application:
            name: borrowservice

        eureka:
          client:
            # 跟上面一样,需要指向Eureka服务端地址,这样才能进行注册
            service-url:
              defaultZone: http://localhost:8888/eureka/

        3.启动类(编码)
        @EnableEurekaClient//开启客户端
        @SpringBootApplication
        public class BorrowApplication {
            public static void main(String[] args) {
                SpringApplication.run(BorrowApplication.class, args);
            }
        }

    b.启动
        客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
        客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication http://127.0.0.1:8888/eureka

02.服务发现
    a.手动将RestTemplate声明为一个Bean,然后添加`@LoadBalanced`注解,这样Eureka就会对服务的调用进行自动发现,并提供负载均衡
        @Configuration
        public class BeanConfig {
            @Bean
            @LoadBalanced
            RestTemplate template(){
                return new RestTemplate();
            }
        }

    b.现在我们怎么实现服务发现呢?
        也就是说,我们之前如果需要对其他微服务进行远程调用,那么就必须要知道其他服务的地址:
        -----------------------------------------------------------------------------------------------------
        User user = template.getForObject("http://localhost:8081/user/"+uid, User.class);
        将localhost:8081替换为【userservice】
        User user = template.getForObject("http://userservice/user/"+uid, User.class);
        -----------------------------------------------------------------------------------------------------
        @Service
        public class BorrowServiceImpl implements BorrowService {
        
            @Resource
            BorrowMapper mapper;
        
            @Resource
            RestTemplate template;
        
            @Override
            public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
                List<Borrow> borrow = mapper.getBorrowsByUid(uid);
                //这里通过调用getForObject来请求其他服务,并将结果自动进行封装
                //获取User信息
                User user = template.getForObject("http://userservice/user/"+uid, User.class);
                //获取每一本书的详细信息
                List<Book> bookList = borrow
                        .stream()
                        .map(b -> template.getForObject("http://bookservice/book/"+b.getBid(), Book.class))
                        .collect(Collectors.toList());
                return new UserBorrowDetail(user, bookList);
            }
        }

    c.LoadBalanced不是说有负载均衡的能力吗,怎么个负载均衡呢?
        同一个服务器实际上是可以注册很多个的,但是它们的端口不同,比如我们这里创建多个用户查询服务,
        我们现在将原有的端口配置修改一下,由IDEA中设定启动参数来决定,这样就可以多创建几个不同端口的启动项了
        客户端  UserApplication-01         环境变量       server.port=8101
        客户端  UserApplication-02         环境变量       server.port=8102
        -----------------------------------------------------------------------------------------------------
        修改一下用户查询,然后进行远程调用,看看请求是不是均匀地分配到这两个服务端:
        @RestController
        public class UserController {
        
            @Resource
            UserService service;
            
            @RequestMapping("/user/{uid}")
            public User findUserById(@PathVariable("uid") int uid){
                System.out.println("我被调用了!");
                return service.getUserById(uid);
            }
        }

    d.启动
        客户端  UserApplication-01         http://127.0.0.1:8101/user/{uid}
        客户端  UserApplication-02         http://127.0.0.1:8102/user/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication    http://127.0.0.1:8888/eureka

    e.测试
        反复操作,http://127.0.0.1:8301/borrow/1
        发现,UserApplication-01、UserApplication-02控制台均出现【我被调用了!】,并且关闭2个中的其中1个,也会正常运行
        这样,服务自动发现以及简单的负载均衡就实现完成了,并且,如果某个微服务挂掉了,只要存在其他同样的微服务实例在运行,那么就不会导致整个微服务不可用,极大地保证了安全性。

1.3 注册中心高可用(集群)

01.注册中心高可用
    虽然Eureka能够实现服务注册和发现,但是如果Eureka服务器崩溃了,岂不是所有需要用到服务发现的微服务就GG了?
    为了避免,这种问题,我们也可以像上面那样,搭建Eureka集群,存在多个Eureka服务器,这样就算挂掉其中一个,
    其他的也还在正常运行,就不会使得服务注册与发现不可用。当然,要是物理黑客直接炸了整个机房,那还是算了吧。

02.Eureka集群(服务端)
    a.application-01.yml
        server:
          port: 8801
        spring:
          application:
            name: eurekaserver
        eureka:
          instance:
            # 由于不支持多个localhost的Eureka服务器,但是又只有本地测试环境,所以就只能自定义主机名称了
            # 主机名称改为eureka01
            hostname: eureka01
          client:
            fetch-registry: false
            # 去掉register-with-eureka选项,让Eureka服务器自己注册到其他Eureka服务器,这样才能相互启用
            service-url:
              # 注意这里填写其他Eureka服务器的地址,不用写自己的
              defaultZone: http://eureka01:8801/eureka
        -----------------------------------------------------------------------------------------------------
        环境变量
            spring.profiles.active=01
    b.application-02.yml
        server:
          port: 8802
        spring:
          application:
            name: eurekaserver
        eureka:
          instance:
            # 由于不支持多个localhost的Eureka服务器,但是又只有本地测试环境,所以就只能自定义主机名称了
            # 主机名称改为eureka02
            hostname: eureka02
          client:
            fetch-registry: false
            # 去掉register-with-eureka选项,让Eureka服务器自己注册到其他Eureka服务器,这样才能相互启用
            service-url:
              # 注意这里填写其他Eureka服务器的地址,不用写自己的
              defaultZone: http://eureka01:8801/eureka
        -----------------------------------------------------------------------------------------------------
        环境变量
            spring.profiles.active=02

03.客户端
    a.用户信息服务
        eureka:
          client:
            service-url:
              defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka
    b.图书管理服务
        eureka:
          client:
            service-url:
              defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka
    c.借阅管理服务
        eureka:
          client:
            service-url:
              defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka

04.启动
    客户端  UserApplication-01         http://127.0.0.1:8101/user/{uid}
    客户端  UserApplication-02         http://127.0.0.1:8102/user/{uid}
    客户端  BookApplication            http://127.0.0.1:8201/book/{bid}
    客户端  BorrowApplication          http://127.0.0.1:8301/borrow/{uid}
    -----------------------------------------------------------------------------------------------------
    服务端  EurekaServer01             http://127.0.0.1:8801
    服务端  EurekaServer02             http://127.0.0.1:8802

2 负载均衡:LoadBalancer

2.1 自定义策略

01.定义
    在2020年前的SpringCloud版本是采用Ribbon作为负载均衡实现
    但是2020年的版本之后SpringCloud把Ribbon移除了,进而用自己编写的LoadBalancer替代

02.实现
    实际上,在添加`@LoadBalanced`注解之后,
    会启用拦截器对我们发起的服务调用请求进行拦截(注意这里是针对我们发起的请求进行拦截),
    叫做`LoadBalancerInterceptor`,它实现`ClientHttpRequestInterceptor`接口:
    @FunctionalInterface
    public interface ClientHttpRequestInterceptor {
        ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
    }
    ---------------------------------------------------------------------------------------------------------
    主要是对`intercept`方法的实现:
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
    ---------------------------------------------------------------------------------------------------------
    服务端会在发起请求时执行这些拦截器。
    那么这个拦截器做了什么事情呢,首先我们要明确,我们给过来的请求地址,并不是一个有效的主机名称,而是服务名称,那么怎么才能得到真正需要访问的主机名称呢,肯定是得找Eureka获取的。
    我们来看看`loadBalancer.execute()`做了什么,它的具体实现为`BlockingLoadBalancerClient`:
    //从上面给进来了服务的名称和具体的请求实体
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        String hint = this.getHint(serviceId);
        LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter(request, new DefaultRequestContext(request, hint));
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);
        supportedLifecycleProcessors.forEach((lifecycle) -> {
            lifecycle.onStart(lbRequest);
        });
        //可以看到在这里会调用choose方法自动获取对应的服务实例信息
        ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);
        if (serviceInstance == null) {
            supportedLifecycleProcessors.forEach((lifecycle) -> {
                lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse()));
            });
            //没有发现任何此服务的实例就抛异常(之前的测试中可能已经遇到了)
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            //成功获取到对应服务的实例,这时就可以发起HTTP请求获取信息了
            return this.execute(serviceId, serviceInstance, lbRequest);
        }
    }
    所以,实际上在进行负载均衡的时候,会向Eureka发起请求,选择一个可用的对应服务,然后会返回此服务的主机地址等信息:

03.LoadBalancer默认提供了两种负载均衡策略:
    RandomLoadBalancer              随机分配策略
    (默认)RoundRobinLoadBalancer    轮询分配策略

04.现在我们希望修改默认的负载均衡策略
    可以进行指定,比如我们现在希望用户服务采用随机分配策略,我们需要先创建随机分配策略的配置类(不用加`@Configuration`)
    public class LoadBalancerConfig {
        //将官方提供的 RandomLoadBalancer 注册为Bean
        @Bean
        public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
            String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
            return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
        }
    }
    ---------------------------------------------------------------------------------------------------------
    接着我们需要为对应的服务指定负载均衡策略,直接使用注解即可:
    @Configuration
    @LoadBalancerClient(value = "userservice",      //指定为 userservice 服务,只要是调用此服务都会使用我们指定的策略
                        configuration = LoadBalancerConfig.class)   //指定我们刚刚定义好的配置类
    public class BeanConfig {
        @Bean
        @LoadBalanced
        RestTemplate template(){
            return new RestTemplate();
        }
    }
    ---------------------------------------------------------------------------------------------------------
    接着我们在`BlockingLoadBalancerClient`中添加断点,观察是否采用我们指定的策略进行请求:
    发现访问userservice服务的策略已经更改为我们指定的策略了。

05.代码实现
    a.LoadBalancerConfig.java
        public class LoadBalancerConfig {
            //将官方提供的 RandomLoadBalancer 注册为Bean
            @Bean
            public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
                String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
                return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
            }
        }
    b.BeanConfig.java
        @Configuration
        @LoadBalancerClient(value = "userservice",                      //指定为 userservice 服务,只要是调用此服务都会使用我们指定的策略
                            configuration = LoadBalancerConfig.class)   //指定我们刚刚定义好的配置类
        public class BeanConfig {
            @Bean
            @LoadBalanced
            RestTemplate template(){
                return new RestTemplate();
            }
        }
    c.BorrowServiceImpl.java
        @Service
        public class BorrowServiceImpl implements BorrowService {

            @Resource
            BorrowMapper mapper;

            @Resource
            RestTemplate template;

            @Override
            public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
                List<Borrow> borrow = mapper.getBorrowsByUid(uid);
                //这里通过调用getForObject来请求其他服务,并将结果自动进行封装
                //获取User信息
                User user = template.getForObject("http://userservice/user/"+uid, User.class);
                //获取每一本书的详细信息
                List<Book> bookList = borrow
                        .stream()
                        .map(b -> template.getForObject("http://bookservice/book/"+b.getBid(), Book.class))
                        .collect(Collectors.toList());
                return new UserBorrowDetail(user, bookList);
            }
        }

2.2 OpenFeign实现负载均衡

01.介绍
    OpenFeign和RestTemplate一样,也是HTTP客户端请求工具,但是它的使用方式更加便捷

02.borrow-service使用
    a.依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    b.启动类
        @EnableFeignClients //开启OpenFeign
        @EnableEurekaClient
        @SpringBootApplication
        public class BorrowApplication {
            public static void main(String[] args) {
                SpringApplication.run(BorrowApplication.class, args);
            }
        }
    c.BookClient接口
        @FeignClient("bookservice")   //声明为userservice服务的HTTP请求客户端
        public interface BookClient {
        
            @RequestMapping("/book/{bid}")
            Book findBookById(@PathVariable("bid") int bid);
        }
    d.UserClient接口
        @FeignClient("userservice")   //声明为userservice服务的HTTP请求客户端
        public interface UserClient {
            //路径保证和其他微服务提供的一致即可
            @RequestMapping("/user/{uid}")
            User findUserById(@PathVariable("uid") int uid);  //参数和返回值也保持一致
        
        }
    e.通过OpenFeign调用BookClient接口、UserClient接口
        @Service
        public class BorrowServiceImpl implements BorrowService {
        
            @Resource
            BorrowMapper mapper;
        
            @Resource
            UserClient userClient;
        
            @Resource
            BookClient bookClient;
        
            @Override
            public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
                List<Borrow> borrow = mapper.getBorrowsByUid(uid);
                //这里通过调用getForObject来请求其他服务,并将结果自动进行封装
                //获取User信息
                User user = userClient.findUserById(uid);
                //获取每一本书的详细信息
                List<Book> bookList = borrow
                    .stream()
                    .map(b -> bookClient.findBookById(b.getBid()))
                    .collect(Collectors.toList());
                return new UserBorrowDetail(user, bookList);
            }
        }
    f.启动
        客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
        客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication http://127.0.0.1:8888/eureka

3 服务降级和熔断:Hystrix

3.1 @HystrixCommand(fallbackMethod = “onError”)

01.介绍
    为了解决分布式系统的雪崩问题,SpringCloud提供了Hystrix熔断器组件,他就像我们家中的保险丝一样,
    当电流过载就会直接熔断,防止危险进一步发生,从而保证家庭用电安全。可以想象一下,如果整条链路上的服务已经全线崩溃,
    这时还在不断地有大量的请求到达,需要各个服务进行处理,肯定是会使得情况越来越糟糕的。
    ---------------------------------------------------------------------------------------------------------
    服务降级:注意一定要区分开服务降级和服务熔断的区别,服务降级并不会直接返回错误,而是可以提供一个补救措施,
             正常响应给请求者。这样相当于服务依然可用,但是服务能力肯定是下降了的。
    服务熔断:熔断机制是应对雪崩效应的一种微服务链路保护机制,当检测出链路的某个微服务不可用或者响应时间太长时,
             会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。
             当检测到该节点微服务响应正常后恢复调用链路。
             实际上,熔断就是在降级的基础上进一步升级形成的,也就是说,在一段时间内多次调用失败,那么就直接升级为熔断。

02.使用
    a.依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>
    b.启动类
        @EnableHystrix      //开启Hystrix
        @EnableFeignClients //开启OpenFeign
        @EnableEurekaClient
        @SpringBootApplication
        public class BorrowApplication {
            public static void main(String[] args) {
                SpringApplication.run(BorrowApplication.class, args);
            }
        }
    c.控制器
        @RestController
        public class BorrowController {

            @Resource
            BorrowService service;

            @HystrixCommand(fallbackMethod = "onError")
            @RequestMapping("/borrow/{uid}")
            UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
                System.out.println("开始向其他服务获取信息");
                return service.getUserBorrowDetailByUid(uid);
            }

            //备选方案,这里直接返回空列表了
            UserBorrowDetail onError(int uid){
                System.out.println("服务错误,进入备选方法!");
                return new UserBorrowDetail(null, Collections.emptyList());
            }
        }
    d.启动
        客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
        客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication http://127.0.0.1:8888/eureka
    e.分析
        服务降级:
        那么现在,由于用户服务和图书服务不可用,所以查询借阅信息的请求肯定是没办法正常响应的,
        这时我们可以提供一个备选方案,也就是说当服务出现异常时,返回我们的备选方案。
        可以看到,虽然我们的服务无法正常运行了,但是依然可以给浏览器正常返回响应数据。
        服务降级是一种比较温柔的解决方案,虽然服务本身的不可用,但是能够保证正常响应数据。
        -----------------------------------------------------------------------------------------------------
        服务熔断:
        一开始的时候,会正常地去调用Controller对应的方法`findUserBorrows`,发现失败然后进入备选方法,
        但是我们发现在持续请求一段时间之后,没有再调用这个方法,而是直接调用备选方案,这便是升级到了熔断状态。
        我们可以继续不断点击,继续不断地发起请求:
        可以看到,过了一段时间之后,会尝试正常执行一次`findUserBorrows`,但是依然是失败状态,所以继续保持熔断状态。
        所以得到结论,它能够对一段时间内出现的错误进行侦测,当侦测到出错次数过多时,熔断器会打开,
        所有的请求会直接响应失败,一段时间后,只执行一定数量的请求,如果还是出现错误,那么则继续保持打开状态,否则说明服务恢复正常运行,关闭熔断器。
        可以看到,当另外两个服务正常运行之后,当再次尝试调用`findUserBorrows`之后会成功,于是熔断机制就关闭了,服务恢复运行。

3.2 @FeignClient(value = “userservice”, fallback = UserFallbackClient.class)

01.介绍
    为了解决分布式系统的雪崩问题,SpringCloud提供了Hystrix熔断器组件,他就像我们家中的保险丝一样,
    当电流过载就会直接熔断,防止危险进一步发生,从而保证家庭用电安全。可以想象一下,如果整条链路上的服务已经全线崩溃,
    这时还在不断地有大量的请求到达,需要各个服务进行处理,肯定是会使得情况越来越糟糕的。
    ---------------------------------------------------------------------------------------------------------
    服务降级:注意一定要区分开服务降级和服务熔断的区别,服务降级并不会直接返回错误,而是可以提供一个补救措施,
             正常响应给请求者。这样相当于服务依然可用,但是服务能力肯定是下降了的。
    服务熔断:熔断机制是应对雪崩效应的一种微服务链路保护机制,当检测出链路的某个微服务不可用或者响应时间太长时,
             会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。
             当检测到该节点微服务响应正常后恢复调用链路。
             实际上,熔断就是在降级的基础上进一步升级形成的,也就是说,在一段时间内多次调用失败,那么就直接升级为熔断。

02.使用
    a.依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>
    b.启动类
        @EnableHystrix      //开启Hystrix
        @EnableFeignClients //开启OpenFeign
        @EnableEurekaClient
        @SpringBootApplication
        public class BorrowApplication {
            public static void main(String[] args) {
                SpringApplication.run(BorrowApplication.class, args);
            }
        }
    c.配置
        feign:
          circuitbreaker:
            enabled: true
    d.创建一个实现类,对原有的接口方法进行替代方案实现
        @Component   //注意,需要将其注册为Bean,Feign才能自动注入
        public class UserFallbackClient implements UserClient{
            @Override
            public User findUserById(int uid) { //这里我们自行对其进行实现,并返回我们的替代方案
                User user = new User();
                user.setName("User替代方案");
                return user;
            }
        }
        -----------------------------------------------------------------------------------------------------
        @Component   //注意,需要将其注册为Bean,Feign才能自动注入
        public class BookFallbackClient implements BookClient{
        
            @Override
            public Book findBookById(int bid) { //这里我们自行对其进行实现,并返回我们的替代方案
                Book book = new Book();
                book.setTitle("Book替代方案");
                return book;
            }
        }
    e.实现完成后,我们只需要在原有的接口中指定失败替代实现即可:
        @FeignClient(value = "userservice", fallback = UserFallbackClient.class)   //声明为userservice服务的HTTP请求客户端
        public interface UserClient {
            //路径保证和其他微服务提供的一致即可
            @RequestMapping("/user/{uid}")
            User findUserById(@PathVariable("uid") int uid);  //参数和返回值也保持一致
        
        }
        -----------------------------------------------------------------------------------------------------
        @FeignClient(value = "bookservice", fallback = BookFallbackClient.class)   //声明为userservice服务的HTTP请求客户端
        public interface BookClient {
        
            @RequestMapping("/book/{bid}")
            Book findBookById(@PathVariable("bid") int bid);
        }
    f.启动
        客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
        客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication http://127.0.0.1:8888/eureka

4 路由网关:Gateway

01.介绍
    一般情况下,可能并不是所有的微服务都需要直接暴露给外部调用,这时我们就可以使用路由机制,添加一层防护,
    让所有的请求全部通过路由来转发到各个微服务,并且转发给多个相同微服务实例也可以实现负载均衡。
    ---------------------------------------------------------------------------------------------------------
    在之前,路由的实现一般使用Zuul,但是已经停更,而现在新出现了由SpringCloud官方开发的Gateway路由,
    它相比Zuul不仅性能上得到了一定的提升,并且是官方推出,契合性也会更好,所以我们这里就主要讲解Gateway。

02.使用
    a.依赖
        <dependencies>
            <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>

            <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
        </dependencies>
    b.启动类
        @EnableEurekaClient
        @SpringBootApplication
        public class GatewayApplication {
            public static void main(String[] args) {
                SpringApplication.run(GatewayApplication.class, args);
            }
        }
    c.配置
        server:
          port: 8401

        spring:
          application:
            name: gateway
          cloud:
            gateway:
              routes:
                - id: borrow-service                     # 路由名称
                  uri: lb://borrowservice                # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
                  predicates:                            # 路由规则,断言什么请求会被路由
                    - Path=/borrow/**                    # 只要是访问的这个路径,一律都被路由到上面指定的服务

                - id: book-service                       # 路由名称
                  uri: lb://bookservice                  # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
                  predicates:                            # 路由规则,断言什么请求会被路由
                    - Path=/book/**                      # 只要是访问的这个路径,一律都被路由到上面指定的服务
                  filters:                               # 添加过滤器
                    - AddRequestHeader=Test, HelloWorld! # 添加请求头信息,其他工厂请查阅官网

                - id: user-service                       # 路由名称
                  uri: lb://userservice                  # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
                  predicates:                            # 路由规则,断言什么请求会被路由
                    - Path=/user/**                      # 只要是访问的这个路径,一律都被路由到上面指定的服务

        eureka:
          client:
            # 跟上面一样,需要指向Eureka服务端地址,这样才能进行注册
            service-url:
              defaultZone: http://localhost:8888/eureka
    d.启动
                                                                       http://127.0.0.1:8401/user/{uid}         客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        网关    GatewayApplication      http://127.0.0.1:8401   ====>  http://127.0.0.1:8401/book/{uid}  ====>  客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
                                                                       http://127.0.0.1:8401/borrow/{uid}       客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication http://127.0.0.1:8888/eureka

5 分布式配置中心:Config

01.介绍
    我们需要一种更加高级的集中化地配置文件管理工具,集中地对配置文件进行配置。
    Spring Cloud Config 为分布式系统中的外部配置提供服务器端和客户端支持。使用 Config Server,您可以集中管理所有环境中应用程序的外部配置
    ---------------------------------------------------------------------------------------------------------
    实际上Spring Cloud Config就是一个配置中心,所有的服务都可以从配置中心取出配置,
    而配置中心又可以从GitHub远程仓库中获取云端的配置文件,这样我们只需要修改GitHub中的配置即可对所有的服务进行配置管理了。

02.服务端
    a.依赖
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>
            
            <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
        </dependencies>
    b.启动类
        @EnableConfigServer  //开启ConfigServer
        @EnableEurekaClient  //开启Eureka
        @SpringBootApplication
        public class ConfigApplication {
            public static void main(String[] args) {
                SpringApplication.run(ConfigApplication.class, args);
            }
        }
    c.配置
        server:
          port: 8501

        spring:
          application:
            name: configserver
          cloud:
            config:
              server:
                git:
                  # 这里填写的是本地仓库地址,远程仓库直接填写远程仓库地址 http://git...
                  # uri: https://github.com/wohenguaii/micro_config_rep.git
                  uri: file://${user.home}/Desktop/config-repo
                  # 默认分支设定为你自己本地或是远程分支的名称
                  default-label: master

        eureka:
          client:
            # 跟上面一样,需要指向Eureka服务端地址,这样才能进行注册
            service-url:
              defaultZone: http://localhost:8888/eureka
    d.访问配置文件
        http://localhost:8501/{服务名称}/{环境}/{Git分支}
        http://localhost:8501/{Git分支}/{服务名称}-{环境}.yml
        -----------------------------------------------------------------------------------------------------
        http://localhost:8501/master/bookservice-prod.yml
        http://localhost:8501/master/borrowservice-prod.yml
        http://localhost:8501/master/eurekaserver-prod.yml
        http://localhost:8501/master/gatewayserver.yml
        http://localhost:8501/master/userservice.yml
    
03.客户端
    那么现在我们的服务既然需要从服务器读取配置文件,那么就需要进行一些配置,我们删除原来的`application.yml`文件
    (也可以保留,最后无论是远端配置还是本地配置都会被加载),改用`bootstrap.yml`(在application.yml之前加载,
    可以实现配置文件远程获取)
    ---------------------------------------------------------------------------------------------------------
    a.依赖
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
            
            <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
        </dependencies>
    b.配置
        spring:
          cloud:
            config:
              # 名称,其实就是文件名称
              name: bookservice
              # 配置服务器的地址
              uri: http://localhost:8501
              # 环境
              profile: prod
              # 分支
              label: master
    c.访问
                                                                       http://127.0.0.1:8401/user/{uid}         客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        网关    GatewayApplication      http://127.0.0.1:8401   ====>  http://127.0.0.1:8401/book/{uid}  ====>  客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
                                                                       http://127.0.0.1:8401/borrow/{uid}       客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  EurekaServerApplication http://127.0.0.1:8888/eureka

6 注册中心/配置:Nacos

6.1 安装

01.介绍
    Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
    简单来说可以当作注册中心,代替Eureka或者ZK;可以当作配置中心,代替Appllo和Config
    
02.下载
    https://github.com/alibaba/nacos/releases/tag/2.0.0-ALPHA.2
    nacos-server-2.0.0-ALPHA.2.tar.gz                                            --已编译Linux和macOS格式
    nacos-server-2.0.0-ALPHA.2.zip                                               --windows版本
    Source code(zip)                                                           --windows版本源码
    Source code (tar.gz)                                                        --Linux和macOS格式源码

03.部署模式
    Nacos支持三种部署模式
    单机模式 - 用于测试和单机试用。
    集群模式 - 用于生产环境,确保高可用。
    多集群模式 - 用于多数据中心场景。

04.conf目录
    nacos-mysql.sql                                                              --初始化SQL
    application.properties                                                       --配置数据库

05.配置conf
    spring.datasource.platform=mysql
    db.num=1
    db.url.0=jdbc:mysql://localhost:3306/ry-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user=root
    db.password=123456

06.启动并访问
    cd C:\software\nacos\bin
    startup.cmd -m standalone                                                    --standalone指定为单机模式,否则以cluster集群模式启动
    ---------------------------------------------------------------------------------------------------------
    http://localhost:8848/nacos
    nacos/nacos

6.2 服务注册与发现

01.服务注册
    a.依赖
        a.父pom.xml
            <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.6.6</version>
                <relativePath/>
            </parent>
            <dependencyManagement>
                    <!-- 这里引入最新的SpringCloud依赖 -->
                    <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-dependencies</artifactId>
                        <version>2021.0.1</version>
                        <type>pom</type>
                        <scope>import</scope>
                    </dependency>

                    <!-- 这里引入最新的SpringCloudAlibaba依赖,2021.0.1.0版本支持SpringBoot2.6.X -->
                    <dependency>
                        <groupId>com.alibaba.cloud</groupId>
                        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                        <version>2021.0.1.0</version>
                        <type>pom</type>
                        <scope>import</scope>
                    </dependency>
                </dependencies>
            </dependencyManagement>
            -----------------------------------------------------------------------------------------------------
            SpringCloud版本和SpringBoot版本不对应
            spring-boot-starter-parent              2.6.6
            spring-cloud-dependencies               2021.0.1
            spring-cloud-alibaba-dependencies       2021.0.1.0
        b.子pom.xml
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    b.服务端
        a.conf目录
            nacos-mysql.sql                                                         --初始化SQL
            application.properties                                                  --配置数据库
        b.配置conf
            spring.datasource.platform=mysql
            db.num=1
            db.url.0=jdbc:mysql://localhost:3306/ry-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
            db.user=root
            db.password=123456
        c.启动并访问
            C:\software\nacos\bin\startup.cmd -m standalone                         --standalone指定为单机模式,否则以cluster集群模式启动
            -------------------------------------------------------------------------------------------------
            http://localhost:8848/nacos
            nacos/nacos
    c.客户端
        a.user-service
            server:
              port: 8101
            spring:
              datasource:
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://127.0.0.1:3306/cloudstudy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
              application:
                name: userservice
              cloud:
                nacos:
                  discovery:
                    server-addr: localhost:8848
        b.book-service
            server:
              port: 8201
            spring:
              datasource:
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://127.0.0.1:3306/cloudstudy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
              application:
                name: bookservice
              cloud:
                nacos:
                  discovery:
                    server-addr: localhost:8848
        c.borrow-service
            server:
              port: 8301
            spring:
              datasource:
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://127.0.0.1:3306/cloudstudy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
              application:
                name: borrowservice
              cloud:
                nacos:
                  discovery:
                    server-addr: localhost:8848

02.服务发现(OpenFeign)
    a.依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    b.启动类
        @EnableFeignClients //开启OpenFeign
        @SpringBootApplication
        public class BorrowApplication {
            public static void main(String[] args) {
                SpringApplication.run(BorrowApplication.class, args);
            }
        }
    c.BookClient接口
        @FeignClient("bookservice")   //声明为userservice服务的HTTP请求客户端
        public interface BookClient {
        
            @RequestMapping("/book/{bid}")
            Book findBookById(@PathVariable("bid") int bid);
        }
    d.UserClient接口
        @FeignClient("userservice")   //声明为userservice服务的HTTP请求客户端
        public interface UserClient {
            //路径保证和其他微服务提供的一致即可
            @RequestMapping("/user/{uid}")
            User findUserById(@PathVariable("uid") int uid);  //参数和返回值也保持一致
        
        }
    e.通过OpenFeign调用BookClient接口、UserClient接口
        @Service
        public class BorrowServiceImpl implements BorrowService {
        
            @Resource
            BorrowMapper mapper;
        
            @Resource
            UserClient userClient;
        
            @Resource
            BookClient bookClient;
        
            @Override
            public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
                List<Borrow> borrow = mapper.getBorrowsByUid(uid);
                //这里通过调用getForObject来请求其他服务,并将结果自动进行封装
                //获取User信息
                User user = userClient.findUserById(uid);
                //获取每一本书的详细信息
                List<Book> bookList = borrow
                    .stream()
                    .map(b -> bookClient.findBookById(b.getBid()))
                    .collect(Collectors.toList());
                return new UserBorrowDetail(user, bookList);
            }
        }
    f.启动
        客户端  UserApplication         http://127.0.0.1:8101/user/{uid}
        客户端  BookApplication         http://127.0.0.1:8201/book/{bid}
        客户端  BorrowApplication       http://127.0.0.1:8301/borrow/{uid}
        -----------------------------------------------------------------------------------------------------
        服务端  Nacos                   http://127.0.0.1:8848/nacos

03.Nacos区分了临时实例和非临时实例
    a.概念
        临时实例:和Eureka一样,采用心跳机制向Nacos发送请求保持在线状态,一旦心跳停止,代表实例下线,不保留实例信息。
        非临时实例:由Nacos主动进行联系,如果连接失败,那么不会移除实例信息,而是将健康状态设定为false,相当于会对某个实例状态持续地进行监控。
    b.配置
        spring:
          application:
            name: borrowservice
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848
                ephemeral: false            # 将ephemeral修改为false,表示非临时实例
    c.管理页面
        接着我们在Nacos中查看,可以发现实例已经不是临时的了。
        如果这时我们关闭此实例,那么只是将健康状态变为false,而不会删除实例的信息。

6.3 配置中心

01.介绍
    在没有配置中心之前,传统应用配置的存在以下痛点:
    (1)采用本地静态配置,无法保证实时性:修改配置不灵活且需要经过较长的测试发布周期,无法尽快通知到客户端,还有些配置对实时性要求很高,比方说主备切换配置或者碰上故障需要修改配置,这时通过传统的静态配置或者重新发布的方式去配置,那么响应速度是非常慢的,业务风险非常大
    (2)易引发生产事故:比如在发布的时候,容易将测试环境的配置带到生产上,引发生产事故。
    (3)配置散乱且格式不标准:有的用properties格式,有的用xml格式,还有的存DB,团队倾向自造轮子,做法五花八门。
    (4)配置缺乏安全审计、版本控制、配置权限控制功能:谁?在什么时间?修改了什么配置?无从追溯,出了问题也无法及时回滚到上一个版本;无法对配置的变更发布进行认证授权,所有人都能修改和发布配置。
    -----------------------------------------------------------------------------------------------------
     而配置中心区别于传统的配置信息分散到系统各个角落的方式,对系统中的配置文件进行集中统一管理,而不需要逐一对单个的服务器进行管理。那这样做有什么好处呢?
    (1)通过配置中心,可以使得配置标准化、格式统一化
    (2)当配置信息发生变动时,修改实时生效,无需要重新重启服务器,就能够自动感知相应的变化,并将新的变化统一发送到相应程序上,快速响应变化。比方说某个功能只是针对某个地区用户,还有某个功能只在大促的时段开放,使用配置中心后只需要相关人员在配置中心动态去调整参数,就基本上可以实时或准实时去调整相关对应的业务。
    (3)通过审计功能还可以追溯问题

02.使用
    a.服务端
        a.userservice-dev.yml
            server:
              port: 8101
            spring:
              datasource:
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://127.0.0.1:3306/cloudstudy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
        b.bookservice-dev.yml
            server:
              port: 8201
            spring:
              datasource:
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://127.0.0.1:3306/cloudstudy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
        c.borrowservice-dev.yml
            server:
              port: 8301
            spring:
              datasource:
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://127.0.0.1:3306/cloudstudy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
    b.客户端
        a.依赖
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            </dependency>
        b.bootstrap.yml
            a.userservice
                spring:
                  application:
                    # 服务名称和配置文件保持一致
                    name: userservice
                  profiles:
                    # 环境也是和配置文件保持一致
                    active: dev
                  cloud:
                    nacos:
                      config:
                        # 配置文件后缀名
                        file-extension: yml
                        # 配置中心服务器地址,也就是Nacos地址
                        server-addr: localhost:8848
            b.bookservice
                spring:
                  application:
                    # 服务名称和配置文件保持一致
                    name: bookservice
                  profiles:
                    # 环境也是和配置文件保持一致
                    active: dev
                  cloud:
                    nacos:
                      config:
                        # 配置文件后缀名
                        file-extension: yml
                        # 配置中心服务器地址,也就是Nacos地址
                        server-addr: localhost:8848
            c.borrowservice
                spring:
                  application:
                    # 服务名称和配置文件保持一致
                    name: borrowservice
                  profiles:
                    # 环境也是和配置文件保持一致
                    active: dev
                  cloud:
                    nacos:
                      config:
                        # 配置文件后缀名
                        file-extension: yml
                        # 配置中心服务器地址,也就是Nacos地址
                        server-addr: localhost:8848

6.4 集群分区、命名空间

01.集群分区
    a.需要分区的微服务在该服务的配置文件
        spring:
          application:
            name: bookservice
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848
                # 集群分区
                cluster-name: Chongqing
        -----------------------------------------------------------------------------------------------------
        spring.cloud.nacos.discovery.cluster-name=chengdu
    b.在调度类提供Nacos的负载均衡的配置
        spring:
          application:
            name: bookservice
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848
                # 集群分区
                cluster-name: Chongqing
            loadbalancer:
              nacos:
                enabled: true
    c.同区域实例也可以单独设置权重
        spring:
          application:
            name: bookservice
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848
                # 集群分区
                cluster-name: Chongqing
                # 权重大小,越大越优先调用,默认为1
                weight: 0.5

02.命名空间
    a.介绍
        public命名空间是nacos的保留空间,默认namespace对应ID为空。即不设置命名空间时候,默认的注册都在public空间下
        一个nacos注册中心的命名空间名具有唯一性,即命名空间名不可以重复。新建命名空间时候,如果不填写命名空间id,则系统会自动生成命名空间id,生成规则为UUID方式
    b.示例
        spring:
          application:
            name: bookservice
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848
                # 集群分区
                cluster-name: Chongqing
                # 权重大小,越大越优先调用,默认为1
                weight: 0.5
                # 命名空间
                namespace: ec979c5e-3253-47d4-86ec-9b417c3f6dcf

7 流量防卫兵:Sentinel

7.1 安装

01.介绍
    一个微服务出现问题,有可能导致整个链路直接不可用,这种时候我们就需要进行及时的熔断和降级,这些策略,我们之前通过使用Hystrix来实现。
    随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
    ---------------------------------------------------------------------------------------------------------
    Sentinel 具有以下特征:
    - 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
    - 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
    - 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
    - 完善的SPI扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

02.安装
    a.下载
        https://github.com/alibaba/Sentinel/releases
    b.启动
        java -jar sentinel-dashboard-1.8.6.jar
    c.访问
        http://localhost:8858/#/dashboard
        sentinel/sentinel

03.使用
    a.依赖
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
    b.配置
        spring:
          cloud:
            sentinel:
              transport:
                # 添加监控页面地址即可
                dashboard: localhost:8858

7.2 流量控制

7.3 服务熔断和降级

8 分布式事务:Seata

8.1 机制介绍

8.2 分布式事务解决方案

8.3 环境搭建

8.3.1 用户服务

01.UserMapper
    @Mapper
    public interface UserMapper {
        @Select("select * from DB_USER where uid = #{uid}")
        User getUserById(int uid);

        @Select("select book_count from DB_USER where uid = #{uid}")
        int getUserBookRemain(int uid);

        @Update("update DB_USER set book_count = #{count} where uid = #{uid}")
        int updateBookCount(int uid, int count);
    }

02.UserServiceImpl
    @Service
    public class UserServiceImpl implements UserService {

        @Resource
        UserMapper mapper;

        @Override
        public User getUserById(int uid) {
            return mapper.getUserById(uid);
        }

        @Override
        public int getRemain(int uid) {
            return mapper.getUserBookRemain(uid);
        }

        @Override
        public boolean setRemain(int uid, int count) {
            return mapper.updateBookCount(uid, count) > 0;
        }
    }
    
03.UserController
    @RestController
    public class UserController {

        @Resource
        UserService service;

        @RequestMapping("/user/{uid}")
        public User findUserById(@PathVariable("uid") int uid){
            return service.getUserById(uid);
        }

        @RequestMapping("/user/remain/{uid}")
        public int userRemain(@PathVariable("uid") int uid){
            return service.getRemain(uid);
        }

        @RequestMapping("/user/borrow/{uid}")
        public boolean userBorrow(@PathVariable("uid") int uid){
            int remain = service.getRemain(uid);
            return service.setRemain(uid, remain - 1);
        }
    }

8.3.2 图书服务

01.BookMapper
    @Mapper
    public interface BookMapper {

        @Select("select * from DB_BOOK where bid = #{bid}")
        Book getBookById(int bid);

        @Select("select count from DB_BOOK  where bid = #{bid}")
        int getRemain(int bid);

        @Update("update DB_BOOK set count = #{count}  where bid = #{bid}")
        int setRemain(int bid, int count);
    }

02.BookServiceImpl
    @Service
    public class BookServiceImpl implements BookService {

        @Resource
        BookMapper mapper;

        @Override
        public Book getBookById(int bid) {
            return mapper.getBookById(bid);
        }

        @Override
        public boolean setRemain(int bid, int count) {
            return mapper.setRemain(bid, count) > 0;
        }

        @Override
        public int getRemain(int bid) {
            return mapper.getRemain(bid);
        }
    }

03.BookController
    @RestController
    public class BookController {

        @Resource
        BookService service;

        @RequestMapping("/book/{bid}")
        Book findBookById(@PathVariable("bid") int bid){
            return service.getBookById(bid);
        }

        @RequestMapping("/book/remain/{bid}")
        public int bookRemain(@PathVariable("bid") int uid){
            return service.getRemain(uid);
        }

        @RequestMapping("/book/borrow/{bid}")
        public boolean bookBorrow(@PathVariable("bid") int uid){
            int remain = service.getRemain(uid);
            return service.setRemain(uid, remain - 1);
        }
    }

8.3.3 借阅服务

01.UserClient
    @FeignClient(value = "userservice")
    public interface UserClient {

        @RequestMapping("/user/{uid}")
        User getUserById(@PathVariable("uid") int uid);

        @RequestMapping("/user/borrow/{uid}")
        boolean userBorrow(@PathVariable("uid") int uid);

        @RequestMapping("/user/remain/{uid}")
        int userRemain(@PathVariable("uid") int uid);
    }

02.BookClient
    @FeignClient("bookservice")
    public interface BookClient {

        @RequestMapping("/book/{bid}")
        Book getBookById(@PathVariable("bid") int bid);

        @RequestMapping("/book/borrow/{bid}")
        boolean bookBorrow(@PathVariable("bid") int bid);

        @RequestMapping("/book/remain/{bid}")
        int bookRemain(@PathVariable("bid") int bid);
    }

04.BorrowController
    @RestController
    public class BorrowController {

        @Resource
        BorrowService service;

        @RequestMapping("/borrow/{uid}")
        UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
            return service.getUserBorrowDetailByUid(uid);
        }

        @RequestMapping("/borrow/take/{uid}/{bid}")
        JSONObject borrow(@PathVariable("uid") int uid,
                          @PathVariable("bid") int bid){
            service.doBorrow(uid, bid);

            JSONObject object = new JSONObject();
            object.put("code", "200");
            object.put("success", false);
            object.put("message", "借阅成功!");
            return object;
        }
    }

05.BorrowServiceImpl
    @Service
    public class BorrowServiceImpl implements BorrowService{

        @Resource
        BorrowMapper mapper;

        @Resource
        UserClient userClient;

        @Resource
        BookClient bookClient;

        @Override
        public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
            List<Borrow> borrow = mapper.getBorrowsByUid(uid);
            User user = userClient.getUserById(uid);
            List<Book> bookList = borrow
                    .stream()
                    .map(b -> bookClient.getBookById(b.getBid()))
                    .collect(Collectors.toList());
            return new UserBorrowDetail(user, bookList);
        }

        @Override
        public boolean doBorrow(int uid, int bid) {
            //1. 判断图书和用户是否都支持借阅
            if(bookClient.bookRemain(bid) < 1)
                throw new RuntimeException("图书数量不足");
            if(userClient.userRemain(uid) < 1)
                throw new RuntimeException("用户借阅量不足");
            //2. 首先将图书的数量-1
            if(!bookClient.bookBorrow(bid))
                throw new RuntimeException("在借阅图书时出现错误!");
            //3. 添加借阅信息
            if(mapper.getBorrow(uid, bid) != null)
                throw new RuntimeException("此书籍已经被此用户借阅了!");
            if(mapper.addBorrow(uid, bid) <= 0)
                throw new RuntimeException("在录入借阅信息时出现错误!");
            //4. 用户可借阅-1
            if(!userClient.userBorrow(uid))
                throw new RuntimeException("在借阅时出现错误!");
            //完成
            return true;
        }
    }

8.3.4 测试

00.说明
    这样,只要我们的图书借阅过程中任何一步出现问题,都会抛出异常。
    我们来测试一下:
    再次尝试借阅,后台会直接报错:
    抛出异常,但是我们发现一个问题,借阅信息添加失败了,但是图书的数量依然被-1,也就是说正常情况下,我们是希望中途出现异常之后,之前的操作全部回滚的:
    而这里由于是在另一个服务中进行的数据库操作,所以传统的`@Transactional`注解无效,这时就得借助Seata提供分布式事务了。

8.4 使用file模式部署

8.4.1 介绍

01.介绍
    Seata服务端支持本地部署或是基于注册发现中心部署(比如Nacos、Eureka等)
    这里我们首先演示一下最简单的本地部署,不需要对Seata的配置文件做任何修改

02.Seata存在着事务分组机制
    事务分组:seata的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
    集群:seata-server服务端一个或多个节点组成的集群cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与Seata服务端集群(默认为default)的映射关系。

03.为啥要设计成通过事务分组再直接映射到集群?
    干嘛不直接指定集群呢?获取事务分组到映射集群的配置
    这样设计后,事务分组可以作为资源的逻辑隔离单位,出现某集群故障时可以快速failover,只切换对应分组
    可以把故障缩减到服务级别,但前提也是你有足够server集群

8.4.2 使用:@GlobalTransactional

01.将我们的各个服务作为Seate的客户端,只需要导入依赖即可
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>

02.添加配置
    seata:
      service:
        vgroup-mapping:
            # 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
            # 这个很关键,一定要配置对,不然会找不到服务
          book-service-seata-service-group: default
        grouplist:
          default: localhost:8091
    seata:
      service:
        vgroup-mapping:
            # 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
            # 这个很关键,一定要配置对,不然会找不到服务
          user-service-seata-service-group: default
        grouplist:
          default: localhost:8091
    seata:
      service:
        vgroup-mapping:
            # 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
            # 这个很关键,一定要配置对,不然会找不到服务
          borrow-service-seata-service-group: default
        grouplist:
          default: localhost:8091

03.配置开启分布式事务,首先在启动类添加注解,此注解会添加一个后置处理器将数据源封装为支持分布式事务的代理数据源(虽然官方表示配置文件中已经默认开启了自动代理,但是UP主实测1.4.2版本下只能打注解的方式才能生效)
    @EnableAutoDataSourceProxy
    @SpringBootApplication
    public class BookApplication {
        public static void main(String[] args) {
            SpringApplication.run(BookApplication.class, args);
        }
    }

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

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

04.在开启分布式事务的方法上添加`@GlobalTransactional`注解
    @GlobalTransactional
    @Override
    public boolean doBorrow(int uid, int bid) {
        //这里打印一下XID看看,其他的服务业添加这样一个打印,如果一会都打印的是同一个XID,表示使用的就是同一个事务
        System.out.println(RootContext.getXID());
        if(bookClient.bookRemain(bid) < 1)
            throw new RuntimeException("图书数量不足");
        if(userClient.userRemain(uid) < 1)
            throw new RuntimeException("用户借阅量不足");
        if(!bookClient.bookBorrow(bid))
            throw new RuntimeException("在借阅图书时出现错误!");
        if(mapper.getBorrow(uid, bid) != null)
            throw new RuntimeException("此书籍已经被此用户借阅了!");
        if(mapper.addBorrow(uid, bid) <= 0)
            throw new RuntimeException("在录入借阅信息时出现错误!");
        if(!userClient.userBorrow(uid))
            throw new RuntimeException("在借阅时出现错误!");
        return true;
    }

05.Seata会分析修改数据的sql,同时生成对应的反向回滚SQL,这个回滚记录会存放在undo_log 表中。所以要求每一个Client 都有一个对应的undo_log表(也就是说每个服务连接的数据库都需要创建这样一个表,这里由于我们三个服务都用的同一个数据库,所以说就只用在这个数据库中创建undo_log表即可),表SQL定义如下:
    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;

06.说明
    创建完成之后,我们现在就可以启动三个服务了,我们来测试一下当出现异常的时候是不是会正常回滚
    首先第一次肯定是正常完成借阅操作的,接着我们再次进行请求,肯定会出现异常
    如果能在栈追踪信息中看到seata相关的包,那么说明分布式事务已经开始工作了,通过日志我们可以看到,出现了回滚操作
    并且数据库中确实是回滚了扣除操作

8.5 使用nacos模式部署

8.5.1 服务端

01.registry.conf
    registry {
      # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
      type = "nacos"

      nacos {
        application = "seata-server"
        serverAddr = "localhost:8848"
        group = "SEATA_GROUP"
        namespace = "61fbe599-6501-4faa-a139-993bc5bfa83a"
        cluster = "default"
        # Nacos的用户名和密码
        username = "nacos"
        password = "nacos"
      }
    }

    config {
      # file、nacos 、apollo、zk、consul、etcd3
      type = "nacos"

      nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = "61fbe599-6501-4faa-a139-993bc5bfa83a"
        group = "SEATA_GROUP"
        username = "nacos"
        password = "nacos"
        dataId = "seataServer.properties"
      }
    }

02.命令行
    C:\Users\mysla\Desktop\TODO5\seata-1.4.2\script\config-center\nacos>sh nacos-config.sh -h 127.0.0.1 -p 8848 -t 61fbe599-6501-4faa-a139-993bc5bfa83a -g SEATA_GROUP
    set nacosAddr=127.0.0.1:8848
    set group=SEATA_GROUP
    Set transport.type=TCP successfully
    Set transport.server=NIO successfully
    Set transport.heartbeat=true successfully
    Set transport.enableClientBatchSendRequest=false successfully
    Set transport.threadFactory.bossThreadPrefix=NettyBoss successfully
    Set transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker successfully
    Set transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler successfully
    Set transport.threadFactory.shareBossWorker=false successfully
    Set transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector successfully
    Set transport.threadFactory.clientSelectorThreadSize=1 successfully
    Set transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread successfully
    Set transport.threadFactory.bossThreadSize=1 successfully
    Set transport.threadFactory.workerThreadSize=default successfully
    Set transport.shutdown.wait=3 successfully
    Set service.vgroupMapping.my_test_tx_group=default successfully
    Set service.default.grouplist=127.0.0.1:8091 successfully
    Set service.enableDegrade=false successfully
    Set service.disableGlobalTransaction=false successfully
    Set client.rm.asyncCommitBufferLimit=10000 successfully
    Set client.rm.lock.retryInterval=10 successfully
    Set client.rm.lock.retryTimes=30 successfully
    Set client.rm.lock.retryPolicyBranchRollbackOnConflict=true successfully
    Set client.rm.reportRetryCount=5 successfully
    Set client.rm.tableMetaCheckEnable=false successfully
    Set client.rm.tableMetaCheckerInterval=60000 successfully
    Set client.rm.sqlParserType=druid successfully
    Set client.rm.reportSuccessEnable=false successfully
    Set client.rm.sagaBranchRegisterEnable=false successfully
    Set client.tm.commitRetryCount=5 successfully
    Set client.tm.rollbackRetryCount=5 successfully
    Set client.tm.defaultGlobalTransactionTimeout=60000 successfully
    Set client.tm.degradeCheck=false successfully
    Set client.tm.degradeCheckAllowTimes=10 successfully
    Set client.tm.degradeCheckPeriod=2000 successfully
    Set store.mode=db successfully
    Set store.publicKey= failure
    Set store.file.dir=file_store/data successfully
    Set store.file.maxBranchSessionSize=16384 successfully
    Set store.file.maxGlobalSessionSize=512 successfully
    Set store.file.fileWriteBufferCacheSize=16384 successfully
    Set store.file.flushDiskMode=async successfully
    Set store.file.sessionReloadReadSize=100 successfully
    Set store.db.datasource=druid successfully
    Set store.db.dbType=mysql successfully
    Set store.db.driverClassName=com.mysql.jdbc.Driver successfully
    Set store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true successfully
    Set store.db.user=root successfully
    Set store.db.password=123456 successfully
    Set store.db.minConn=5 successfully
    Set store.db.maxConn=30 successfully
    Set store.db.globalTable=global_table successfully
    Set store.db.branchTable=branch_table successfully
    Set store.db.queryLimit=100 successfully
    Set store.db.lockTable=lock_table successfully
    Set store.db.maxWait=5000 successfully
    Set store.redis.mode=single successfully
    Set store.redis.single.host=127.0.0.1 successfully
    Set store.redis.single.port=6379 successfully
    Set store.redis.sentinel.masterName= failure
    Set store.redis.sentinel.sentinelHosts= failure
    Set store.redis.maxConn=10 successfully
    Set store.redis.minConn=1 successfully
    Set store.redis.maxTotal=100 successfully
    Set store.redis.database=0 successfully
    Set store.redis.password= failure
    Set store.redis.queryLimit=100 successfully
    Set server.recovery.committingRetryPeriod=1000 successfully
    Set server.recovery.asynCommittingRetryPeriod=1000 successfully
    Set server.recovery.rollbackingRetryPeriod=1000 successfully
    Set server.recovery.timeoutRetryPeriod=1000 successfully
    Set server.maxCommitRetryTimeout=-1 successfully
    Set server.maxRollbackRetryTimeout=-1 successfully
    Set server.rollbackRetryTimeoutUnlockEnable=false successfully
    Set client.undo.dataValidation=true successfully
    Set client.undo.logSerialization=jackson successfully
    Set client.undo.onlyCareUpdateColumns=true successfully
    Set server.undo.logSaveDays=7 successfully
    Set server.undo.logDeletePeriod=86400000 successfully
    Set client.undo.logTable=undo_log successfully
    Set client.undo.compress.enable=true successfully
    Set client.undo.compress.type=zip successfully
    Set client.undo.compress.threshold=64k successfully
    Set log.exceptionRate=100 successfully
    Set transport.serialization=seata successfully
    Set transport.compressor=none successfully
    Set metrics.enabled=false successfully
    Set metrics.registryType=compact successfully
    Set metrics.exporterList=prometheus successfully
    Set metrics.exporterPrometheusPort=9898 successfully
    =========================================================================
     Complete initialization parameters,  total-count:89 ,  failure-count:4
    =========================================================================
     init nacos config fail.

03.将对应的事务组映射配置也添加上,Dataldi格式为【service:vgroupMapping.事务组名称】
    service:vgroupMapping.book-service-seata-service-group : SEATA_GROUP : default
    service:vgroupMapping.user-service-seata-service-group : SEATA_GROUP : default
    service:vgroupMapping.borrow-service-seata-service-group : SEATA_GROUP : default

8.5.2 客户端

01.将我们的各个服务作为Seate的客户端,只需要导入依赖即可:
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>

02.配置
    seata:
      # 注册
      registry:
        # 使用Nacos
        type: nacos
        nacos:
          # 使用Seata的命名空间,这样才能正确找到Seata服务,由于组使用的是SEATA_GROUP,配置默认值就是,就不用配了
          namespace: 61fbe599-6501-4faa-a139-993bc5bfa83a
          username: nacos
          password: nacos
      # 配置
      config:
        type: nacos
        nacos:
          namespace: 61fbe599-6501-4faa-a139-993bc5bfa83a
          username: nacos
          password: nacos

03.配置开启分布式事务,首先在启动类添加注解,此注解会添加一个后置处理器将数据源封装为支持分布式事务的代理数据源(虽然官方表示配置文件中已经默认开启了自动代理,但是UP主实测1.4.2版本下只能打注解的方式才能生效)
    @EnableAutoDataSourceProxy
    @SpringBootApplication
    public class BookApplication {
        public static void main(String[] args) {
            SpringApplication.run(BookApplication.class, args);
        }
    }

04.在开启分布式事务的方法上添加`@GlobalTransactional`注解
    @GlobalTransactional
    @Override
    public boolean doBorrow(int uid, int bid) {
        //这里打印一下XID看看,其他的服务业添加这样一个打印,如果一会都打印的是同一个XID,表示使用的就是同一个事务
        System.out.println(RootContext.getXID());
        if(bookClient.bookRemain(bid) < 1)
            throw new RuntimeException("图书数量不足");
        if(userClient.userRemain(uid) < 1)
            throw new RuntimeException("用户借阅量不足");
        if(!bookClient.bookBorrow(bid))
            throw new RuntimeException("在借阅图书时出现错误!");
        if(mapper.getBorrow(uid, bid) != null)
            throw new RuntimeException("此书籍已经被此用户借阅了!");
        if(mapper.addBorrow(uid, bid) <= 0)
            throw new RuntimeException("在录入借阅信息时出现错误!");
        if(!userClient.userBorrow(uid))
            throw new RuntimeException("在借阅时出现错误!");
        return true;
    }

8.5.3 数据库

01.配置
    store.mode : db
    store.session.mode : db

02.表说明
    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    CREATE TABLE IF NOT EXISTS `global_table`
    (
        `xid`                       VARCHAR(128) NOT NULL,
        `transaction_id`            BIGINT,
        `status`                    TINYINT      NOT NULL,
        `application_id`            VARCHAR(32),
        `transaction_service_group` VARCHAR(32),
        `transaction_name`          VARCHAR(128),
        `timeout`                   INT,
        `begin_time`                BIGINT,
        `application_data`          VARCHAR(2000),
        `gmt_create`                DATETIME,
        `gmt_modified`              DATETIME,
        PRIMARY KEY (`xid`),
        KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;

    -- the table to store BranchSession data
    CREATE TABLE IF NOT EXISTS `branch_table`
    (
        `branch_id`         BIGINT       NOT NULL,
        `xid`               VARCHAR(128) NOT NULL,
        `transaction_id`    BIGINT,
        `resource_group_id` VARCHAR(32),
        `resource_id`       VARCHAR(256),
        `branch_type`       VARCHAR(8),
        `status`            TINYINT,
        `client_id`         VARCHAR(64),
        `application_data`  VARCHAR(2000),
        `gmt_create`        DATETIME(6),
        `gmt_modified`      DATETIME(6),
        PRIMARY KEY (`branch_id`),
        KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;

    -- the table to store lock data
    CREATE TABLE IF NOT EXISTS `lock_table`
    (
        `row_key`        VARCHAR(128) NOT NULL,
        `xid`            VARCHAR(128),
        `transaction_id` BIGINT,
        `branch_id`      BIGINT       NOT NULL,
        `resource_id`    VARCHAR(256),
        `table_name`     VARCHAR(32),
        `pk`             VARCHAR(36),
        `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_status` (`status`),
        KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;

    CREATE TABLE IF NOT EXISTS `distributed_lock`
    (
        `lock_key`       CHAR(20) NOT NULL,
        `lock_value`     VARCHAR(20) NOT NULL,
        `expire`         BIGINT,
        primary key (`lock_key`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;

    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('HandleAllSession', ' ', 0);