В этой статье мыSpringCloud
окружающей среды с помощьюSeata
выдавать себя за пользователя购买商品
время из-за пользователяНедостаточный балансВ результате сбоя отправки этого заказа, чтобы проверить следующийMySQL
Будут ли транзакции в базе данных回滚
.
Статьи в этой главе охватывают только те из них, которые необходимо протестировать.服务列表
а такжеSeata
Раздел конфигурации.
Пользователь отправляет заказ на покупку продукта, который условно делится на следующие этапы:
- уменьшить запас
- вычтенная сумма
- Отправить заказы
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выпуск!