前言
在上一篇中,我們簡單聊了聊分佈式事務的問題和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模式下整合的全部步驟,細節尚未考考慮周全之處敬請諒解,最後感謝觀看!