springboot整合seata

前言

在上一篇中,我們簡單聊了聊分佈式事務的問題和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進行着心跳包的交換
在這裏插入圖片描述

首先在數據庫的storage表初始化一條數據
在這裏插入圖片描述

假設當前商品數量上5000個,如果購買10個,則可以購買成功,同時積分也會增加,訂單增加一條數據

分別調用接口:http://localhost:8000/test1 【正常的接口】

分別調用接口:http://localhost:8000/test2 【數據異常的接口】

然後再去觀察數據庫數據,在調用接口2的時候,在分佈式環境下,扣減庫存的操作會失敗,加上了@GlobalTransactional註解之後,可以發現數據庫的3張表沒有產生中間數據,即通過seata-server操作的undo_log表完成了數據的回滾,從而解決了這個問題,具體的效果可以在本地調用接口後,去數據庫觀察undo_log表的數據
在這裏插入圖片描述

以上便是使用seata在AT模式下整合的全部步驟,細節尚未考考慮周全之處敬請諒解,最後感謝觀看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章