SpringBoot實戰電商項目mall(25k+star)地址:github.com/macrozheng/…java
Seata是Alibaba開源的一款分佈式事務解決方案,致力於提供高性能和簡單易用的分佈式事務服務,本文將經過一個簡單的下單業務場景來對其用法進行詳細介紹。node
單體應用中,一個業務操做須要調用三個模塊完成,此時數據的一致性由本地事務來保證。mysql
隨着業務需求的變化,單體應用被拆分紅微服務應用,原來的三個模塊被拆分紅三個獨立的應用,分別使用獨立的數據源,業務操做須要調用三個服務來完成。此時每一個服務內部的數據一致性由本地事務來保證,可是全局的數據一致性問題無法保證。git
在微服務架構中因爲全局數據一致性無法保證產生的問題就是分佈式事務問題。簡單來講,一次業務操做須要操做多個數據源或須要進行遠程調用,就會產生分佈式事務問題。github
Seata 是一款開源的分佈式事務解決方案,致力於提供高性能和簡單易用的分佈式事務服務。Seata 將爲用戶提供了 AT、TCC、SAGA 和 XA 事務模式,爲用戶打造一站式的分佈式解決方案。redis
咱們能夠把一個分佈式事務理解成一個包含了若干分支事務的全局事務,全局事務的職責是協調其下管轄的分支事務達成一致,要麼一塊兒成功提交,要麼一塊兒失敗回滾。此外,一般分支事務自己就是一個知足ACID的本地事務。這是咱們對分佈式事務結構的基本認識,與 XA 是一致的。spring
咱們先從官網下載seata-server,這裏下載的是seata-server-0.9.0.zip
,下載地址:github.com/seata/seata…sql
這裏咱們使用Nacos做爲註冊中心,Nacos的安裝及使用能夠參考:Spring Cloud Alibaba:Nacos 做爲註冊中心和配置中心使用;數據庫
解壓seata-server安裝包到指定目錄,修改conf
目錄下的file.conf
配置文件,主要修改自定義事務組名稱,事務日誌存儲模式爲db
及數據庫鏈接信息;bash
service {
#vgroup->rgroup
vgroup_mapping.fsp_tx_group = "default" #修改事務組名稱爲:fsp_tx_group,和客戶端自定義的名稱對應
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
## transaction log store
store {
## store mode: file、db
mode = "db" #修改此處將事務信息存儲到數據庫中
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seat-server" #修改數據庫鏈接地址
user = "root" #修改數據庫用戶名
password = "root" #修改數據庫密碼
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
複製代碼
因爲咱們使用了db模式存儲事務日誌,因此咱們須要建立一個seat-server數據庫,建表sql在seata-server的/conf/db_store.sql
中;
修改conf
目錄下的registry.conf
配置文件,指明註冊中心爲nacos
,及修改nacos
鏈接信息便可;
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd三、sofa
type = "nacos" #改成nacos
nacos {
serverAddr = "localhost:8848" #改成nacos的鏈接地址
namespace = ""
cluster = "default"
}
}
複製代碼
/bin/seata-server.bat
文件啓動seata-server。CREATE TABLE `order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金額',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '訂單狀態:0:建立中;1:已完結' AFTER `money` ;
複製代碼
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩餘庫存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seat-storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
複製代碼
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用餘額',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩餘可用額度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seat-account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
複製代碼
使用Seata還須要在每一個數據庫中建立日誌表,建表sql在seata-server的/conf/db_undo_log.sql
中。
這裏咱們會建立三個服務,一個訂單服務,一個庫存服務,一個帳戶服務。當用戶下單時,會在訂單服務中建立一個訂單,而後經過遠程調用庫存服務來扣減下單商品的庫存,再經過遠程調用帳戶服務來扣減用戶帳戶裏面的餘額,最後在訂單服務中修改訂單狀態爲已完成。該操做跨越三個數據庫,有兩次遠程調用,很明顯會有分佈式事務問題。
對seata-order-service、seata-storage-service和seata-account-service三個seata的客戶端進行配置,它們配置大體相同,咱們下面以seata-order-service的配置爲例;
修改application.yml文件,自定義事務組的名稱;
spring:
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group #自定義事務組名稱須要與seata-server中的對應
複製代碼
service {
#vgroup->rgroup
vgroup_mapping.fsp_tx_group = "default" #修改自定義事務組名稱
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
複製代碼
registry {
# file 、nacos 、eureka、redis、zk
type = "nacos" #修改成nacos
nacos {
serverAddr = "localhost:8848" #修改成nacos的鏈接地址
namespace = ""
cluster = "default"
}
}
複製代碼
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderServiceApplication.class, args);
}
}
複製代碼
/** * 使用Seata對數據源進行代理 * Created by macro on 2019/11/11. */
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
複製代碼
package com.macro.cloud.service.impl;
import com.macro.cloud.dao.OrderDao;
import com.macro.cloud.domain.Order;
import com.macro.cloud.service.AccountService;
import com.macro.cloud.service.OrderService;
import com.macro.cloud.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/** * 訂單業務實現類 * Created by macro on 2019/11/11. */
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private OrderDao orderDao;
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
/** * 建立訂單->調用庫存服務扣減庫存->調用帳戶服務扣減帳戶餘額->修改訂單狀態 */
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
LOGGER.info("------->下單開始");
//本應用建立訂單
orderDao.create(order);
//遠程調用庫存服務扣減庫存
LOGGER.info("------->order-service中扣減庫存開始");
storageService.decrease(order.getProductId(),order.getCount());
LOGGER.info("------->order-service中扣減庫存結束:{}",order.getId());
//遠程調用帳戶服務扣減餘額
LOGGER.info("------->order-service中扣減餘額開始");
accountService.decrease(order.getUserId(),order.getMoney());
LOGGER.info("------->order-service中扣減餘額結束");
//修改訂單狀態爲已完成
LOGGER.info("------->order-service中修改訂單狀態開始");
orderDao.update(order.getUserId(),0);
LOGGER.info("------->order-service中修改訂單狀態結束");
LOGGER.info("------->下單結束");
}
}
複製代碼
運行seata-order-service、seata-storage-service和seata-account-service三個服務;
數據庫初始信息狀態:
/** * 帳戶業務實現類 * Created by macro on 2019/11/11. */
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
@Autowired
private AccountDao accountDao;
/** * 扣減帳戶餘額 */
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣減帳戶餘額開始");
//模擬超時異常,全局事務回滾
try {
Thread.sleep(30*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId,money);
LOGGER.info("------->account-service中扣減帳戶餘額結束");
}
}
複製代碼
/** * 訂單業務實現類 * Created by macro on 2019/11/11. */
@Service
public class OrderServiceImpl implements OrderService {
/** * 建立訂單->調用庫存服務扣減庫存->調用帳戶服務扣減帳戶餘額->修改訂單狀態 */
@Override
// @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
LOGGER.info("------->下單開始");
//省略代碼...
LOGGER.info("------->下單結束");
}
}
複製代碼
Seata官方文檔:github.com/seata/seata…
springcloud-learning
├── seata-order-service -- 整合了seata的訂單服務
├── seata-storage-service -- 整合了seata的庫存服務
└── seata-account-service -- 整合了seata的帳戶服務
複製代碼
mall項目全套學習教程連載中,關注公衆號第一時間獲取。