在上一篇中,我们简单聊了聊分布式事务的问题和seata的基本介绍,在使用seata实现分布式事务的解决方案中,提供了常用的3种模式,AT模式,TCC模式和saga模式,并且说明了AT模式下的使用原理,下面对AT模式下,使用springboot与seata整合解决分布式事务的问题,做简单的介绍
1、官网下载seata-server-1.0.0.zip
上文谈AT模式时候提到了一个TC,即事务协调者,seata-server即一个后台服务,启动之后管理并协调全局的事务,它的作用原理很简单,我们可以类比zookeeper,当一个集群启动之后,zookeeper是怎么管理其他节点的呢?就是不断的与各个节点发送和接受心跳包,那么seata-server理解也是如此
下载之后解压出来,如果使用默认的配置,直接接入bin目录,在windows环境下,直接双击.bat文件即可启动
但本文的环境构建基于springcloud,因此各个微服务之间的交互打算通过eureka注册中心进行RPC的调用,因此需要在conf目录下简单配置一下
重点需要关注的上图中的两个文件,如果要使用eureka作为服务的注册中心,需要修改的地方如下,即在registry的配置文件中做如下修改,默认的type是file类型
这个不难理解,就是说当seata-server启动的时候,这个服务要以什么形式注册到哪里去,seata提供了多种方式,比如redis,zk,nacos等,可以根据自己的实际情况做选择,nacos也是一个不错的选择,属于阿里开源的分布式配置中心
而file.conf配置文件要关注的上图中的几行,简单解释就是这个创建的全局事务协调器管理的各个本地事务,对他们进行分组,这一seata-server才能通过这个全局事务协调各个分支事务,更加详细的配置解释官网都有说明,可以参考学习
2、业务场景
eureka-server:服务注册中心
business:业务服务,即完成TM的功能,在该服务中进行全局的事务控制
order:下单服务
points:积分服务
storage:库存服务
3、数据库准备
各个微服务创建自己的数据库,同时为了TM管理全局的事务,需要在每个数据库创建一个undo_log表,官网提供了执行sql
创建一个聚合maven项目,包括5个模块的子工程,如上图所示,
1、eureka-server 工程
服务注册中心,这个没什么要说的,主要是在配置文件yml的配置
server:
port: 8761
eureka:
server:
enable-self-preservation: false
instance:
appname: provider-service
hostname: localhost
client:
service-url:
defaultZone:
http://localhost:8761/eureka/
register-with-eureka: false
fetch-registry: false
spring:
main:
allow-bean-definition-overriding: true
pom依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.11.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerStarter {
public static void main(String[] args) {
SpringApplication.run(EurekaServerStarter.class, args);
}
}
启动eureka-server,再启动上面已经配置好的seata-server,然后浏览器访问:http://localhost:8761/
可以看到eureka启动成功,同时seata-server也注册到了eureka中
2、order、storage、points 工程
这三个微服务各自负责处理自己的业务逻辑,整合过程中流程基本上一样,下面列举其中一个进行说明,以storage工程为例
2.1 添加pom依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.2 配置yml
server:
port: 8003
spring:
application:
name: storage-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://IP:3306/storage?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: user
password: user
将file.conf文件以及registry文件拷贝至resources目录
2.3 配置数据源代码
说明:seata在AT模式下解决分布式事务的具体逻辑体现在对数据源的代理上,即对DataSource产生代理变成DataSourceProxy,进行全局事务的管理和协调,因此在整合时,需通过配置类的方式进行配置
/**
* 数据源代理
*/
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource") //6.1 创建DataSourceProxy
public DataSourceProxy dataSourceProxy(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean //6.2 将原有的DataSource对象替换为DataSourceProxy
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
到这里,基本的配置工作已经接近尾声,下面只需要添加本工程自己的业务逻辑即可,由于需要对外提供服务,本工程上库存模块,提供一个对外接口,即扣减库存的接口
controller层
@RestController
public class StorageController {
@Resource
private StorageService storageService;
@GetMapping("/decrease")
public Storage decrease(@RequestParam(required = false)String goodsCode,
@RequestParam(required = false)Integer quantity){
return storageService.decrease(goodsCode, quantity);
}
}
service业务实现层
@Service
public class StorageService {
@Resource
private StorageDAO storageDAO;
/**
* 减少库存
* @param goodsCode 商品编码
* @param quantity 减少数量
* @return 库存对象
*/
public Storage decrease(String goodsCode, Integer quantity) {
Storage storage = storageDAO.findByGoodsCode(goodsCode);
if (storage.getQuantity() >= quantity) {
storage.setQuantity(storage.getQuantity() - quantity);
}else{
throw new RuntimeException(goodsCode + "库存不足,目前剩余库存:" + storage.getQuantity() );
}
storageDAO.update(storage);
return storage;
}
}
接口:
@Repository
@Mapper
public interface StorageDAO {
public Storage findByGoodsCode(String goodsCode);
public void update(Storage storage);
}
启动类:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@MapperScan(basePackages="com.congge.storage.dao")
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
按照上述同样的流程将points以及order工程配置完毕即可
3、business 工程
该工程提供对外访问的入口,即创建订单的操作,同时也是TM即事务管理器的地方,在本工程中,需要通过rpc的方式调用order、points、storage等接口,为使用方便,这里使用springcloud中的open-feign进行调用,
3.1 pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
其他依赖可参考上述的
3.2 yml配置
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
mybatis:
mapperLocations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
server:
port: 8000
spring:
application:
name: bussiness-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://IP:3306/business?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: user
password: user
logging:
level:
io:
seata: debug
3.3 DataSource配置【同上】
3.4 对外提供调用接口
controller层
@RestController
public class BussinessController {
@Resource
private BussinessService bussinessService;
@GetMapping("/test1")
public Order test1(){
return bussinessService.sale("coke",10,"zhangsan",3,30f);
}
@GetMapping("/test2")
public Order test2(){
return bussinessService.sale("coke",10000,"zhangsan",3000,30000f);
}
}
service层 ,要注意的上@GlobalTransactional注解,通过这个注解来控制全局事务
@Service
public class BussinessService {
@Autowired
private PointsServiceClient pointsServiceClient;
@Autowired
private StorageServiceClient storageServiceClient;
@Autowired
private OrderServiceClient orderServiceClient;
/**
* 商品销售
* @param goodsCode 商品编码
* @param quantity 销售数量
* @param username 用户名
* @param points 增加积分
* @param amount 订单金额
* @return 订单对象
*/
@GlobalTransactional(name = "fsp-sale" , timeoutMills = 20000 , rollbackFor = Exception.class)
//@Transactional
public Order sale(String goodsCode , Integer quantity ,String username ,Integer points, Float amount ){
pointsServiceClient.increase(username, points);
storageServiceClient.decrease(goodsCode, quantity);
Order order = orderServiceClient.create(goodsCode, quantity, username, points, amount);
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return order;
}
}
feign客户端接口层
@FeignClient("points-service")
public interface PointsServiceClient {
@GetMapping("/increase")
public Points increase(@RequestParam(value = "username",required = false) String username,
@RequestParam(value = "quantity",required = false) Integer quantity);
}
@FeignClient("storage-service")
public interface StorageServiceClient {
@GetMapping("/decrease")
public Storage decrease(
@RequestParam(value="goodsCode",required = false) String goodsCode,
@RequestParam(value = "quantity",required = false) Integer quantity);
}
@FeignClient("order-service")
public interface OrderServiceClient {
@GetMapping("/create")
public Order create(@RequestParam(value="goodsCode",required = false) String goodsCode ,
@RequestParam(value = "quantity",required = false) Integer quantity ,
@RequestParam(value = "username",required = false) String username ,
@RequestParam(value = "points",required = false) Integer points,
@RequestParam(value = "amount",required = false) Float amount );
}
启动类:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class BussinessApplication {
public static void main(String[] args) {
SpringApplication.run(BussinessApplication.class, args);
}
}
到这里演示工程的搭建以及配置都全部搞定了,下面将项目全部启动,启动过程中,注意这行日志,反应的就是各个服务和seata-server进行着心跳包的交换
假设当前商品数量上5000个,如果购买10个,则可以购买成功,同时积分也会增加,订单增加一条数据
分别调用接口:http://localhost:8000/test1 【正常的接口】
分别调用接口:http://localhost:8000/test2 【数据异常的接口】
然后再去观察数据库数据,在调用接口2的时候,在分布式环境下,扣减库存的操作会失败,加上了@GlobalTransactional注解之后,可以发现数据库的3张表没有产生中间数据,即通过seata-server操作的undo_log表完成了数据的回滚,从而解决了这个问题,具体的效果可以在本地调用接口后,去数据库观察undo_log表的数据
以上便是使用seata在AT模式下整合的全部步骤,细节尚未考考虑周全之处敬请谅解,最后感谢观看!