Первый опыт распределенных транзакций SpringCloud и Seata

Spring Boot

В этой статье мыSpringCloudокружающей среды с помощьюSeataвыдавать себя за пользователя购买商品время из-за пользователяНедостаточный балансВ результате сбоя отправки этого заказа, чтобы проверить следующийMySQLБудут ли транзакции в базе данных回滚.

Статьи в этой главе охватывают только те из них, которые необходимо протестировать.服务列表а такжеSeataРаздел конфигурации.

Пользователь отправляет заказ на покупку продукта, который условно делится на следующие этапы:

  1. уменьшить запас
  2. вычтенная сумма
  3. Отправить заказы

1. Подготовьте среду

  • Seata Server

    если правильноSeata ServerМетод развертывания пока неизвестен, пожалуйста, посетите:blog.with rest.com/springcloud…

  • Eureka Server

    Сервисный реестр, если правильноEureka ServerМетод развертывания пока неизвестен, пожалуйста, посетитеblog.with rest.com/springcloud…

2. Подготовьте тестовый сервис

Чтобы облегчить студентам просмотр исходного кода, мы используем исходный код в этой главе.Maven Module(многомодульный) способ сборки.

Сторонние зависимости, используемые службами, которые мы используем для тестирования, непротиворечивы, иpom.xmlСодержимое файла следующее:

<dependencies>
  <!--Web-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!--openfeign接口定义-->
  <dependency>
    <groupId>org.minbox.chapter</groupId>
    <artifactId>openfeign-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </dependency>
  <!--公共依赖-->
  <dependency>
    <groupId>org.minbox.chapter</groupId>
    <artifactId>common-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </dependency>

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

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

  <!--Eureka Client-->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>

  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <dependency>
    <groupId>org.minbox.framework</groupId>
    <artifactId>api-boot-starter-mybatis-enhance</artifactId>
  </dependency>
</dependencies>

2.1 Модуль определения интерфейса Openfeign

Из-за использованияOpenfeignспособ звонить друг другу, поэтому создается модульopenfeign-serviceпредоставлять服务接口的定义.

  • Определение интерфейса, предоставляемое службой учетных записей

账户服务предоставленный извнеOpenfeignОпределение интерфейса выглядит так:

/**
 * 账户服务接口
 *
 * @author 恒宇少年
 */
@FeignClient(name = "account-service")
@RequestMapping(value = "/account")
public interface AccountClient {
    /**
     * 扣除指定账户金额
     *
     * @param accountId 账户编号
     * @param money     金额
     */
    @PostMapping
    void deduction(@RequestParam("accountId") Integer accountId, @RequestParam("money") Double money);
}
  • Определение интерфейса, предоставляемое товарным сервисом

    商品服务предоставленный извнеOpenfeignОпределение интерфейса выглядит так:

    /**
     * 商品服务接口定义
     *
     * @author 恒宇少年
     */
    @FeignClient(name = "good-service")
    @RequestMapping(value = "/good")
    public interface GoodClient {
        /**
         * 查询商品基本信息
         *
         * @param goodId {@link Good#getId()}
         * @return {@link Good}
         */
        @GetMapping
        Good findById(@RequestParam("goodId") Integer goodId);
    
        /**
         * 减少商品的库存
         *
         * @param goodId {@link Good#getId()}
         * @param stock  减少库存的数量
         */
        @PostMapping
        void reduceStock(@RequestParam("goodId") Integer goodId, @RequestParam("stock") int stock);
    }
    

2.2 Общие модули

общедоступный модульcommon-serviceКлассы, представленные в рамках共用的, можно вызвать каждую службу, наиболее важной из которых являетсяSeataПредоставленный прокси источника данных (DataSourceProxy) Конфигурация создания экземпляра находится в этом модуле, а код конфигурации, относящийся к агенту базы данных, выглядит следующим образом:

/**
 * Seata所需数据库代理配置类
 *
 * @author 恒宇少年
 */
@Configuration
public class DataSourceProxyAutoConfiguration {
    /**
     * 数据源属性配置
     * {@link DataSourceProperties}
     */
    private DataSourceProperties dataSourceProperties;

    public DataSourceProxyAutoConfiguration(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    /**
     * 配置数据源代理,用于事务回滚
     *
     * @return The default datasource
     * @see DataSourceProxy
     */
    @Primary
    @Bean("dataSource")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperties.getUrl());
        dataSource.setUsername(dataSourceProperties.getUsername());
        dataSource.setPassword(dataSourceProperties.getPassword());
        dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        return new DataSourceProxy(dataSource);
    }
}

Этот класс конфигурации используется в требуемом сервисе@ImportАннотации импортируются для использования.

2.3 Услуги по учетным записям

  • Реализация сервисного интерфейса

    账户服务Реализация службы для предоставления интерфейса путем реализацииopenfeign-serviceпредоставляется в течениеAccountClientСлужба определяет интерфейс для предоставления соответствующей реализации службы.Интерфейс реализации выглядит следующим образом:

    /**
     * 账户接口实现
     *
     * @author 恒宇少年
     */
    @RestController
    public class AccountController implements AccountClient {
        /**
         * 账户业务逻辑
         */
        @Autowired
        private AccountService accountService;
    
        @Override
        public void deduction(Integer accountId, Double money) {
            accountService.deduction(accountId, money);
        }
    }
    
  • Конфигурация службы (application.yml)

    # 服务名
    spring:
      application:
        name: account-service
      # seata分组
      cloud:
        alibaba:
          seata:
            tx-service-group: minbox-seata
      # 数据源
      datasource:
        url: jdbc:mysql://localhost:3306/test
        username: root
        password: 123456
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    # eureka
    eureka:
      client:
        service-url:
          defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
    

    пройти черезspring.cloud.alibaba.seata.tx-service-groupМы можем указать группу транзакций, к которой принадлежит сервис.Эта конфигурация не является обязательной и по умолчаниюspring.application.nameСодержимое конфигурации плюс строка-fescar-service-group,как:account-service-fescar-service-group, видетьcom.alibaba.cloud.seata.GlobalTransactionAutoConfigurationИсходный код класса конфигурации.

    в моей локальной тестовой средеEureka Serverсуществует10.180.98.83На сервере это необходимо изменить на ваш собственный адрес, а информацию о соединении с базой данных также необходимо изменить в соответствии с вашей собственной конфигурацией.

  • Импорт конфигурации агента источника данных Seata

    /**
     * @author 恒宇少年
     */
    @SpringBootApplication
    @Import(DataSourceProxyAutoConfiguration.class)
    public class AccountServiceApplication {
        /**
         * logger instance
         */
        static Logger logger = LoggerFactory.getLogger(AccountServiceApplication.class);
    
        public static void main(String[] args) {
            SpringApplication.run(AccountServiceApplication.class, args);
            logger.info("账户服务启动成功.");
        }
    }
    

    пройти через@Importимпортировать насcommon-serviceпредоставляется в течениеSeataКласс конфигурации прокси источника данныхDataSourceProxyAutoConfiguration.

2.4 Товары и услуги

  • Реализация сервисного интерфейса

    Товарный сервис предоставляет услуги интерфейса запроса товаров и вычета запасов для достиженияopenfeign-serviceкоторый предоставилGoodClientОпределение интерфейса службы выглядит следующим образом:

    /**
     * 商品接口定义实现
     *
     * @author 恒宇少年
     */
    @RestController
    public class GoodController implements GoodClient {
        /**
         * 商品业务逻辑
         */
        @Autowired
        private GoodService goodService;
    
        /**
         * 查询商品信息
         *
         * @param goodId {@link Good#getId()}
         * @return
         */
        @Override
        public Good findById(Integer goodId) {
            return goodService.findById(goodId);
        }
    
        /**
         * 扣减商品库存
         *
         * @param goodId {@link Good#getId()}
         * @param stock  减少库存的数量
         */
        @Override
        public void reduceStock(Integer goodId, int stock) {
            goodService.reduceStock(goodId, stock);
        }
    }
    
  • Конфигурация службы (application.yml)

    spring:
      application:
        name: good-service
      cloud:
        alibaba:
          seata:
            tx-service-group: minbox-seata
      datasource:
        url: jdbc:mysql://localhost:3306/test
        username: root
        password: 123456
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    
    eureka:
      client:
        service-url:
          defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
    server:
      port: 8081
    
  • Импорт конфигурации агента источника данных Seata

    /**
     * @author 恒宇少年
     */
    @SpringBootApplication
    @Import(DataSourceProxyAutoConfiguration.class)
    public class GoodServiceApplication {
        /**
         * logger instance
         */
        static Logger logger = LoggerFactory.getLogger(GoodServiceApplication.class);
    
        public static void main(String[] args) {
            SpringApplication.run(GoodServiceApplication.class, args);
            logger.info("商品服务启动成功.");
        }
    }
    

2.5 Услуга заказа

  • сервисный интерфейс

    订单服务Предоставляет интерфейс для размещения заказа. При вызове этого интерфейса функция оформления заказа завершается. Интерфейс оформления заказа пройдетOpenfeignперечислитьaccount-service,good-serviceПредоставленный сервисный интерфейс для завершения проверки данных выглядит следующим образом:

    /**
     * @author 恒宇少年
     */
    @RestController
    @RequestMapping(value = "/order")
    public class OrderController {
        /**
         * 账户服务接口
         */
        @Autowired
        private AccountClient accountClient;
        /**
         * 商品服务接口
         */
        @Autowired
        private GoodClient goodClient;
        /**
         * 订单业务逻辑
         */
        @Autowired
        private OrderService orderService;
    
        /**
         * 通过{@link GoodClient#reduceStock(Integer, int)}方法减少商品的库存,判断库存剩余数量
         * 通过{@link AccountClient#deduction(Integer, Double)}方法扣除商品所需要的金额,金额不足由account-service抛出异常
         *
         * @param goodId    {@link Good#getId()}
         * @param accountId {@link Account#getId()}
         * @param buyCount  购买数量
         * @return
         */
        @PostMapping
        @GlobalTransactional
        public String submitOrder(
                @RequestParam("goodId") Integer goodId,
                @RequestParam("accountId") Integer accountId,
                @RequestParam("buyCount") int buyCount) {
    
            Good good = goodClient.findById(goodId);
    
            Double orderPrice = buyCount * good.getPrice();
    
            goodClient.reduceStock(goodId, buyCount);
    
            accountClient.deduction(accountId, orderPrice);
    
            Order order = toOrder(goodId, accountId, orderPrice);
            orderService.addOrder(order);
            return "下单成功.";
        }
    
        private Order toOrder(Integer goodId, Integer accountId, Double orderPrice) {
            Order order = new Order();
            order.setGoodId(goodId);
            order.setAccountId(accountId);
            order.setPrice(orderPrice);
            return order;
        }
    }
    
  • Конфигурация службы (application.yml)

    spring:
      application:
        name: order-service
      cloud:
        alibaba:
          seata:
            tx-service-group: minbox-seata
      datasource:
        url: jdbc:mysql://localhost:3306/test
        username: root
        password: 123456
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    
    eureka:
      client:
        service-url:
          defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
    server:
      port: 8082
    
  • Включить Openfeign и импортировать конфигурацию прокси-сервера источника данных Seata

    /**
     * @author 恒宇少年
     */
    @SpringBootApplication
    @EnableFeignClients(basePackages = "org.minbox.chapter.seata.openfeign")
    @Import(DataSourceProxyAutoConfiguration.class)
    public class OrderServiceApplication {
        /**
         * logger instance
         */
        static Logger logger = LoggerFactory.getLogger(OrderServiceApplication.class);
    
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
            logger.info("订单服务启动成功.");
        }
    }
    

    мы толькоorder-serviceвызов других службOpenfeignинтерфейс, поэтому нам нужно толькоorder-serviceпроходить внутри@EnableFeignClientsАннотации включеныOpenfeignИнтерфейс реализует прокси.

3. Сервисное подключение к Seata Server

служба хочет подключиться кSeata ServerНеобходимо добавить два файла конфигурации, а именноregistry.conf,file.conf.

  • registry.conf

    зарегистрироваться вSeata ServerКонфигурационный файл, который содержит метод регистрации и метод чтения конфигурационного файла, выглядит следующим образом:

    registry {
      # file、nacos、eureka、redis、zk、consul
      type = "file"
    
      file {
        name = "file.conf"
      }
    
    }
    
    config {
      type = "file"
    
      file {
        name = "file.conf"
      }
    }
    
  • file.conf

    Файл конфигурации содержит использованиеfileспособ подключения кEureka Serverинформация о конфигурации и存储分布式事务信息следующим образом:

    transport {
      # tcp udt unix-domain-socket
      type = "TCP"
      #NIO NATIVE
      server = "NIO"
      #enable heartbeat
      heartbeat = true
      #thread factory for netty
      thread-factory {
        boss-thread-prefix = "NettyBoss"
        worker-thread-prefix = "NettyServerNIOWorker"
        server-executor-thread-prefix = "NettyServerBizHandler"
        share-boss-worker = false
        client-selector-thread-prefix = "NettyClientSelector"
        client-selector-thread-size = 1
        client-worker-thread-prefix = "NettyClientWorkerThread"
        # netty boss thread size,will not be used for UDT
        boss-thread-size = 1
        #auto default pin or 8
        worker-thread-size = 8
      }
    }
    ## transaction log store
    store {
      ## store mode: file、db
      mode = "file"
    
      ## file store
      file {
        dir = "sessionStore"
    
        # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
        max-branch-session-size = 16384
        # globe session size , if exceeded throws exceptions
        max-global-session-size = 512
        # file buffer size , if exceeded allocate new buffer
        file-write-buffer-cache-size = 16384
        # when recover batch read size
        session.reload.read_size = 100
        # async, sync
        flush-disk-mode = async
      }
    
      ## database store
      db {
        datasource = "druid"
        db-type = "mysql"
        driver-class-name = "com.mysql.jdbc.Driver"
        url = "jdbc:mysql://10.180.98.83:3306/iot-transactional"
        user = "dev"
        password = "dev2019."
      }
    
    }
    service {
      vgroup_mapping.minbox-seata = "default"
      default.grouplist = "10.180.98.83:8091"
      enableDegrade = false
      disable = false
    }
    client {
      async.commit.buffer.limit = 10000
      lock {
        retry.internal = 10
        retry.times = 30
      }
    }
    

    в файле конфигурацииserviceотметить, что мы находимся вapplication.ymlГруппировка транзакций настраивается в файле конфигурации какminbox-seata, здесь требуется соответствующая конфигурацияvgroup_mapping.minbox-seata = "default",пройти черезdefault.grouplist = "10.180.98.83:8091"настроитьSeata Serverперечень услуг.

Поместите указанные выше два файла конфигурации в каждую службу.resourcesдиректория создана.

4. Напишите логику заказа

Столько уже было сказано, но мы только проделали подготовительную работу, нужно добавить соответствующую бизнес-логику для каждой услуги, участвующей в заказе.

  • Услуги по работе с учетными записями

    существуетaccount-serviceДобавьте класс бизнес-логики вычета баланса счета,AccountServiceСледующее:

    /**
     * 账户业务逻辑处理
     *
     * @author 恒宇少年
     */
    @Service
    @Transactional(rollbackFor = Exception.class)
    public class AccountService {
        
        @Autowired
        private EnhanceMapper<Account, Integer> mapper;
    
        /**
         * {@link EnhanceMapper} 具体使用查看ApiBoot官网文档http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html
         *
         * @param accountId {@link Account#getId()}
         * @param money     扣除的金额
         */
        public void deduction(Integer accountId, Double money) {
            Account account = mapper.selectOne(accountId);
            if (ObjectUtils.isEmpty(account)) {
                throw new RuntimeException("账户:" + accountId + ",不存在.");
            }
            if (account.getMoney() - money < 0) {
                throw new RuntimeException("账户:" + accountId + ",余额不足.");
            }
            account.setMoney(account.getMoney().doubleValue() - money);
            mapper.update(account);
        }
    }
    
  • товарный сервис

    существуетgood-serviceДобавьте логические классы для запроса товаров и вычета товарных запасов,GoodServiceСледующее:

    /**
     * 商品业务逻辑实现
     *
     * @author 恒宇少年
     */
    @Service
    @Transactional(rollbackFor = Exception.class)
    public class GoodService {
    
        @Autowired
        private EnhanceMapper<Good, Integer> mapper;
    
        /**
         * 查询商品详情
         *
         * @param goodId {@link Good#getId()}
         * @return {@link Good}
         */
        public Good findById(Integer goodId) {
            return mapper.selectOne(goodId);
        }
    
        /**
         * {@link EnhanceMapper} 具体使用查看ApiBoot官网文档http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html
         * 扣除商品库存
         *
         * @param goodId {@link Good#getId()}
         * @param stock  扣除的库存数量
         */
        public void reduceStock(Integer goodId, int stock) {
            Good good = mapper.selectOne(goodId);
            if (ObjectUtils.isEmpty(good)) {
                throw new RuntimeException("商品:" + goodId + ",不存在.");
            }
            if (good.getStock() - stock < 0) {
                throw new RuntimeException("商品:" + goodId + "库存不足.");
            }
            good.setStock(good.getStock() - stock);
            mapper.update(good);
    
        }
    }
    

5. Отправить тест заказа

Имеем в базе перед выполнением тестаseata_account,seata_goodСоответственно добавьте два тестовых данных в таблицу, как показано ниже:

-- seata_good
INSERT INTO `seata_good` VALUES (1,'华为Meta 30',10,5000.00);	

-- seata_account
INSERT INTO `seata_account` VALUES (1,10000.00,'2019-10-11 02:37:35',NULL);

5.1 Запустите службу

будет использоваться в этой главеgood-server,order-service,account-serviceЗапущены три службы.

5.2 Контрольная точка: обычная покупка

Добавленных нами тестовых данных баланса счета достаточно для покупки двух предметов.Давайте сначала купим один предмет, чтобы проверить, успешен ли доступ к интерфейсу.Доступ к интерфейсу заказа с помощью следующей команды:

~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=1
下单成功.

доступ через нас/orderИнтерфейс заказа, по содержанию ответа определяем, что товар успешно куплен.

просмотревorder-serviceСодержимое консоли:

2019-10-11 16:52:15.477  INFO 13142 --- [nio-8082-exec-4] i.seata.tm.api.DefaultGlobalTransaction  : [10.180.98.83:8091:2024417333] commit status:Committed
2019-10-11 16:52:16.412  INFO 13142 --- [atch_RMROLE_2_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=10.180.98.83:8091:2024417333,branchId=2024417341,branchType=AT,resourceId=jdbc:mysql://localhost:3306/test,applicationData=null
2019-10-11 16:52:16.412  INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler            : Branch committing: 10.180.98.83:8091:2024417333 2024417341 jdbc:mysql://localhost:3306/test null
2019-10-11 16:52:16.412  INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed

Мы видим, что эта транзакция прошла успешно.Committed.

для проверки базы данных账户余额,商品库存Есть ли вычеты.

5.3 Контрольная точка: недостаточный инвентарь

Добавлен тестовый элемент10Есть инвентарь, и в предыдущем тесте был продан товар.Проверяем, есть ли журнал отката, когда купленное количество превышает количество инвентаря, и выполняем следующую команду:

~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=10
{"timestamp":"2019-10-11T08:57:13.775+0000","status":500,"error":"Internal Server Error","message":"status 500 reading GoodClient#reduceStock(Integer,int)","path":"/order"}

в нашемgood-serviceСервисная консоль напечатала аномальную информацию о недостаточных запасах:

java.lang.RuntimeException: 商品:1库存不足.
	at org.minbox.chapter.seata.service.GoodService.reduceStock(GoodService.java:42) ~[classes/:na]
	....

Посмотримorder-serviceЖурнал печати консоли:

Begin new global transaction [10.180.98.83:8091:2024417350]
2019-10-11 16:57:13.771  INFO 13142 --- [nio-8082-exec-5] i.seata.tm.api.DefaultGlobalTransaction  : [10.180.98.83:8091:2024417350] rollback status:Rollbacked

Вы можете просмотреть ход этой транзакции через журнал回滚.

так какПроверка инвентаря перед вычетом остатка на счете, поэтому на этот раз мы не можем судить по данным базы данных, что транзакция действительно откатилась.

5.4 Контрольная точка: недостаточный баланс

Поскольку инвентаря продукта недостаточно, мы не можем напрямую проверить, что транзакция базы данных откатывается. Мы начинаем с недостаточного баланса учетной записи. Мы успешно приобрели продукт ранее, и баланса учетной записи все еще достаточно для покупки продукта. Инвентаризация продукта в настоящее время9件, мы покупаем этот тест5件пункт, так что пункт покупки появится库存充足и余额不足сценарий приложения, выполните следующую команду, чтобы инициировать запрос:

~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=5
{"timestamp":"2019-10-11T09:03:00.794+0000","status":500,"error":"Internal Server Error","message":"status 500 reading AccountClient#deduction(Integer,Double)","path":"/order"}

Мы смотрим наaccount-serviceЖурнал консоли показывает:

java.lang.RuntimeException: 账户:1,余额不足.
	at org.minbox.chapter.seata.service.AccountService.deduction(AccountService.java:33) ~[classes/:na]

уже брошенный余额不足исключение.

просмотревgood-service,order-serivceВ журнале консоли видно, что транзакция откатилась.

Проверить следующееseata_accountтабличных данных мы обнаружили, что баланс счета не изменился, сервис счета事务回滚Проверка прошла успешно.

Проверятьseata_goodПо табличным данным мы обнаружили, что товарно-материальные запасы не изменились, товары и услуги事务回滚Проверка прошла успешно.

6. Резюме

Эта глава предназначена в основном для проверки структуры распределенных транзакций.SeataсуществуетMySQLПроверяем правильность коммита и отката, может ли он дать ожидаемый нами эффект,Seataв видеSpringCloud AlibabaБазовая структура часто обновляется, и она может быстро решить проблемы, возникающие в процессе использования.Это потенциальный запас и хороший выбор.

Поскольку в этой главе разработано много кодов, пожалуйста, изучите исходный код.

7. Исходный код этой главы

Пожалуйста, посетитеgit ee.com/sh-wave-with/spr…Просмотрите исходный код этой главы, рекомендуется использоватьgit clone https://gitee.com/hengboy/spring-cloud-chapter.gitЗагрузите исходный код локально.

Эта статья опубликована в блогеOpenWriteвыпуск!