给大家分享一下阿里的分布式事务框架Seata的完整搭建教程,我感觉这篇教程已经算是很详细了,基本上每个必须的依赖和配置项都写的明明白白,为了让大家能更简单的先上手运行,注册中心和微服务框架均使用阿里系即Nacos和Dubbo,相关框架服务均使用最新稳定版进行搭建。好了,废话不多说了,开始实操。
sh startup.sh -m standalone
如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:
bash startup.sh -m standalone
Windows
启动命令(standalone代表着单机模式运行,非集群模式):
startup.cmd -m standalone
启动完毕以后在浏览器中访问nacos默认后台管理地址:http://127.0.0.1:8848/nacos,默认登录账号和密码均为nacos,成功登录后表示nacos服务的搭建已完成,如图
1.首先根据文档提示下载Seata服务包【官方Github下载地址】
2. 解压以后修改\conf\registry.conf配置文件,注册中心修改为nacos注册中心
配置信息使用默认的file,然后修改\conf\file.conf,将事务存储信息选用redis
3. 进去bin目录执行启动脚本
./seata-server.bat
启动成功后可在nacos的服务列表中看到seata的服务已经注册进去注册中心了
到此,环境准备工作就完毕了,接下来开始进行项目的搭建
使用idea直接new一个SpringBoot项目,只需要引入“Spring Web”即可,项目名称为microservice-user
<!-- Nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.1</version> <exclusions> <exclusion> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>2.0.3</version> </dependency>
PS:这里需要特别注意:nacos存在对应的版本关系,所以这里项目的SpringBoot版本必须调整为2.4.2,否则可能会导致项目无法正常启动
spring: application: name: microservice-user cloud: nacos: discovery: server-addr: localhost:8848 # namespace: public # 默认的即为public空间,为了简便,所有服务的创建配置都使用的默认项 username: nacos password: nacos server: port: 8000
<properties> <java.version>1.8</java.version> <dubbo.version>3.0.3</dubbo.version> </properties> <!-- Dubbo配置 --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>${dubbo.version}</version> <exclusions> <exclusion> <artifactId>spring</artifactId> <groupId>org.springframework</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>${dubbo.version}</version> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> <version>${dubbo.version}</version> </dependency>
dubbo: application: name: ${spring.application.name} scan: base-packages: com.cn.lucky.morning.user.service.api registry: address: nacos://${spring.cloud.nacos.discovery.server-addr} username: ${spring.cloud.nacos.discovery.username} password: ${spring.cloud.nacos.discovery.password}
/** * @author lucky_morning */ public interface AccountService { /** * 从用户账户中借出 * * @param userId 用户ID * @param money 金额 */ void debit(String userId, int money); }
在com.cn.lucky.morning.user.api包下新增接口实现类,实现上一步的接口类并在类上使用@DubboService将类标注为服务提供方
@DubboService @Service public class AccountServiceImpl implements AccountService { @Autowired private ITblAccountService tblAccountService; @Override public void debit(String userId, int money) { TblAccount account = tblAccountService.getById(userId); if (account == null) { throw new RuntimeException("用户不存在"); } account.setMoney(account.getMoney() - money); if (account.getMoney() < 0) { throw new RuntimeException("用户余额不足"); } tblAccountService.updateById(account); } }
/** * @author lucky_morning */ @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; /** * 创建订单 */ @GetMapping("/create") public String create(String userId, String commodityCode, int orderCount) { try { return "订单创建结果:" + orderService.create(userId, commodityCode, orderCount); }catch (Exception e){ return "订单创建失败:" + e.getMessage(); } } }
访问地址:http://localhost:8001/order/create?userId=1&commodityCode=phone&orderCount=1
因为account数据表现在都是空表,并未插入数据,所以微服务调用用户减少金额时会抛出用户不存在的异常,证明已经正常在order微服务中调用了user微服务提供的方法
<!-- Seata 配置 --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2021.1</version> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency>
seata: config: type: nacos nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} username: ${spring.cloud.nacos.discovery.username} password: ${spring.cloud.nacos.discovery.password} data-id: seataServer.properties registry: type: nacos nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} username: ${spring.cloud.nacos.discovery.username} password: ${spring.cloud.nacos.discovery.password}
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tbl_account`; CREATE TABLE `tbl_account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
package com.cn.lucky.morning.user.api; /** * @author lucky_morning */ public interface AccountService { /** * 从用户账户中借出 * * @param userId 用户ID * @param money 金额 */ void debit(String userId, int money); }
package com.cn.lucky.morning.user.service.api; import com.cn.lucky.morning.user.api.AccountService; import com.cn.lucky.morning.user.entity.TblAccount; import com.cn.lucky.morning.user.service.ITblAccountService; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @DubboService @Service public class AccountServiceImpl implements AccountService { @Autowired private ITblAccountService tblAccountService; @Override public void debit(String userId, int money) { TblAccount account = tblAccountService.getById(userId); if (account == null) { throw new RuntimeException("用户不存在"); } account.setMoney(account.getMoney() - money); if (account.getMoney() < 0) { throw new RuntimeException("用户余额不足"); } tblAccountService.updateById(account); } }
DROP TABLE IF EXISTS `tbl_order`; CREATE TABLE `tbl_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
package com.cn.lucky.morning.order.api; /** * @author lucky_morning */ public interface OrderService { /** * 创建订单 * @return */ boolean create(String userId, String commodityCode, int orderCount); }
package com.cn.lucky.morning.order.service.api; import com.cn.lucky.morning.order.api.OrderService; import com.cn.lucky.morning.order.entity.TblOrder; import com.cn.lucky.morning.order.service.ITblOrderService; import com.cn.lucky.morning.user.api.AccountService; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author lucky_morning */ @Service @DubboService public class OrderServiceImpl implements OrderService { @Autowired private ITblOrderService tblOrderService; @DubboReference private AccountService accountService; @Override public boolean create(String userId, String commodityCode, int orderCount) { int orderMoney = calculate(commodityCode, orderCount); accountService.debit(userId, orderMoney); TblOrder order = new TblOrder(); order.setId(Integer.valueOf(userId)); order.setCommodityCode(commodityCode); order.setCount(orderCount); order.setMoney(orderMoney); return tblOrderService.save(order); } /** * 计算价格 * * @param commodityCode 商品Code * @param orderCount 商品数量 * @return 购买价格 */ private int calculate(String commodityCode, int orderCount) { return 100 * orderCount; } }
DROP TABLE IF EXISTS `tbl_storage`; CREATE TABLE `tbl_storage` ( `id` int(11) NOT NULL AUTO_INCREMENT, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY (`commodity_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
package com.cn.lucky.morning.storage.service.impl; import com.cn.lucky.morning.storage.entity.TblStorage; import com.cn.lucky.morning.storage.mapper.TblStorageMapper; import com.cn.lucky.morning.storage.service.ITblStorageService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; /** * <p> * 服务实现类 * </p> * * @author lucky_morning * @since 2021-10-20 */ @Service public class TblStorageServiceImpl extends ServiceImpl<TblStorageMapper, TblStorage> implements ITblStorageService { @Override public void deduct(String commodityCode, int count) { TblStorage storage = this.lambdaQuery().eq(TblStorage::getCommodityCode, commodityCode).last("limit 1").one(); if (storage == null) { throw new RuntimeException("商品不存在"); } if (storage.getCount() < count) { throw new RuntimeException("商品库存不足"); } storage.setCount(storage.getCount() - count); this.updateById(storage); } }
package com.cn.lucky.morning.storage.service.impl; import com.cn.lucky.morning.order.api.OrderService; import com.cn.lucky.morning.storage.service.BusinessService; import com.cn.lucky.morning.storage.service.ITblStorageService; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author lucky_morning */ @Service public class BusinessServiceImpl implements BusinessService { @Autowired private ITblStorageService storageService; @DubboReference private OrderService orderService; @Override public void purchase(String userId, String commodityCode, int orderCount) { // 减库存 storageService.deduct(commodityCode,orderCount); // 新增订单 orderService.create(userId, commodityCode, orderCount); } }
package com.cn.lucky.morning.storage.controller; import com.cn.lucky.morning.storage.service.BusinessService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * <p> * 前端控制器 * </p> * * @author lucky_morning * @since 2021-10-20 */ @RestController @RequestMapping("/business") public class BusinessController { @Autowired private BusinessService businessService; /** * 采购 * * @param userId 采购用户 * @param commodityCode 商品Code * @param orderCount 商品数量 * @return 采购结果 */ @GetMapping("/purchase") public String purchase(String userId, String commodityCode, int orderCount) { try { businessService.purchase(userId, commodityCode, orderCount); return "操作成功"; } catch (Exception e) { return "操作失败:" + e.getMessage(); } } }
调用成功,那么三个数据表的变化分别为,tbl_account表中,用户ID为1的账号金额减少100;tbl_order表中新增一条订单记录,tbl_storage表中,商品Code为phone的库存减少1,在这种理论正常的情况下,各个微服务之间调用都不出错,你好我好大家都好,但现实往往都不是这么美好的,会出现各种未知的情况导致中途失败,比如接下来几个例子。
继续使用上面的数据表数据,我们将请求的商品数量参数修改为3,那么此时库存有4是能正常取货,但是用户金额是200,金额不够
接口返回了用户余额不足,那我们再来看一下数据表的变化呢
因为用户减少金额是在库存减少之后,所以异常抛出之后商品库存依然减少了
首先我们将数据库里面的数据还原到上一步,即把库存的数据修改回4,在采购方法上增加分布式事务注解后重启storage微服务
重启成功后再次调用接口
此时我们再回到数据库查看数据,会发现数据表中数据都没有发生变化,并且我们在storage微服务的控制台上还能看到seata打印的分布式事务相关的信息
此时,分布式事务的集成就算完成了,可以看出当一个方法中调用了多个微服务的提供方方式对不同的数据表进行操作的时候,在中途某一步出错了,如果没有使用分布式事务,那么出错之前对数据库的所有操作都不会被还原,就会导致数据不一致的情况出现,而使用了分布式事务之后,多个微服务的调用也像以前的数据库事务一样简单,要么都成功,要么都失败还原
这就是最简单的项目集成示例,示例项目相关代码我已经放在GitHub上了,访问地址:https://github.com/luckymorning/SpringBootNacosSeataDubboDemoProject
如有不对之处,欢迎大家指正交流