在本篇文章中咱們在SpringCloud
環境下經過使用Seata
來模擬用戶購買商品
時因爲用戶餘額不足致使本次訂單提交失敗,來驗證下在MySQL
數據庫內事務是否會回滾
。html
本章文章只涉及所須要測試的服務列表
以及Seata
配置部分。java
用戶提交訂單購買商品大體分爲如下幾個步驟:node
Seata Servermysql
若是對Seata Server
部署方式還不瞭解,請訪問:blog.yuqiyu.com/SpringCloud…git
Eureka Serverweb
服務註冊中心,若是對Eureka Server
部署方式還不瞭解,請訪問blog.yuqiyu.com/SpringCloud…redis
爲了方便學習的同窗查看源碼,咱們本章節源碼採用Maven Module
(多模塊)的方式進行構建。spring
咱們用於測試的服務所使用的第三方依賴都一致,各個服務的pom.xml
文件內容以下所示:sql
<dependencies>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--openfeign接口定義-->
<dependency>
<groupId>org.minbox.chapter</groupId>
<artifactId>openfeign-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--公共依賴-->
<dependency>
<groupId>org.minbox.chapter</groupId>
<artifactId>common-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--Eureka Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.minbox.framework</groupId>
<artifactId>api-boot-starter-mybatis-enhance</artifactId>
</dependency>
</dependencies>
複製代碼
因爲咱們服務之間採用的Openfeign
方式進行相互調用,因此建立了一個模塊openfeign-service
來提供服務接口的定義
。數據庫
帳戶服務
對外所提供的Openfeign
接口定義以下所示:
/** * 帳戶服務接口 * * @author 恆宇少年 */
@FeignClient(name = "account-service")
@RequestMapping(value = "/account")
public interface AccountClient {
/** * 扣除指定帳戶金額 * * @param accountId 帳戶編號 * @param money 金額 */
@PostMapping
void deduction(@RequestParam("accountId") Integer accountId, @RequestParam("money") Double money);
}
複製代碼
商品服務提供的接口定義
商品服務
對外所提供的Openfeign
接口定義以下所示:
/** * 商品服務接口定義 * * @author 恆宇少年 */
@FeignClient(name = "good-service")
@RequestMapping(value = "/good")
public interface GoodClient {
/** * 查詢商品基本信息 * * @param goodId {@link Good#getId()} * @return {@link Good} */
@GetMapping
Good findById(@RequestParam("goodId") Integer goodId);
/** * 減小商品的庫存 * * @param goodId {@link Good#getId()} * @param stock 減小庫存的數量 */
@PostMapping
void reduceStock(@RequestParam("goodId") Integer goodId, @RequestParam("stock") int stock);
}
複製代碼
公共模塊common-service
內所提供的類是共用的
,各個服務均可以調用,其中最爲重要的是將Seata
所提供的數據源代理(DataSourceProxy
)實例化配置放到了這個模塊中,數據庫代理相關配置代碼以下所示:
/** * Seata所需數據庫代理配置類 * * @author 恆宇少年 */
@Configuration
public class DataSourceProxyAutoConfiguration {
/** * 數據源屬性配置 * {@link DataSourceProperties} */
private DataSourceProperties dataSourceProperties;
public DataSourceProxyAutoConfiguration(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
/** * 配置數據源代理,用於事務回滾 * * @return The default datasource * @see DataSourceProxy */
@Primary
@Bean("dataSource")
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dataSourceProperties.getUrl());
dataSource.setUsername(dataSourceProperties.getUsername());
dataSource.setPassword(dataSourceProperties.getPassword());
dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
return new DataSourceProxy(dataSource);
}
}
複製代碼
該配置類在所須要的服務中使用@Import
註解進行導入使用。
服務接口實現
帳戶服務
用於提供接口的服務實現,經過實現openfeign-service
內提供的AccountClient
服務定義接口來對應提供服務實現,實現接口以下所示:
/** * 帳戶接口實現 * * @author 恆宇少年 */
@RestController
public class AccountController implements AccountClient {
/** * 帳戶業務邏輯 */
@Autowired
private AccountService accountService;
@Override
public void deduction(Integer accountId, Double money) {
accountService.deduction(accountId, money);
}
}
複製代碼
服務配置(application.yml)
# 服務名
spring:
application:
name: account-service
# seata分組
cloud:
alibaba:
seata:
tx-service-group: minbox-seata
# 數據源
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
# eureka
eureka:
client:
service-url:
defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
複製代碼
經過spring.cloud.alibaba.seata.tx-service-group
咱們能夠指定服務所屬事務的分組,該配置非必填,默認爲spring.application.name
配置的內容加上字符串-fescar-service-group
,如:account-service-fescar-service-group
,詳見com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration
配置類源碼。
在我本地測試環境的
Eureka Server
在10.180.98.83
服務器上,這裏須要修改爲大家本身的地址,數據庫鏈接信息也須要修改爲大家本身的配置。
導入Seata數據源代理配置
/** * @author 恆宇少年 */
@SpringBootApplication
@Import(DataSourceProxyAutoConfiguration.class)
public class AccountServiceApplication {
/** * logger instance */
static Logger logger = LoggerFactory.getLogger(AccountServiceApplication.class);
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class, args);
logger.info("帳戶服務啓動成功.");
}
}
複製代碼
經過@Import
導入咱們common-service
內提供的Seata
數據源代理配置類DataSourceProxyAutoConfiguration
。
服務接口實現
商品服務提供商品的查詢以及庫存扣減接口服務,實現openfeign-service
提供的GoodClient
服務接口定義以下所示:
/** * 商品接口定義實現 * * @author 恆宇少年 */
@RestController
public class GoodController implements GoodClient {
/** * 商品業務邏輯 */
@Autowired
private GoodService goodService;
/** * 查詢商品信息 * * @param goodId {@link Good#getId()} * @return */
@Override
public Good findById(Integer goodId) {
return goodService.findById(goodId);
}
/** * 扣減商品庫存 * * @param goodId {@link Good#getId()} * @param stock 減小庫存的數量 */
@Override
public void reduceStock(Integer goodId, int stock) {
goodService.reduceStock(goodId, stock);
}
}
複製代碼
服務配置(application.yml)
spring:
application:
name: good-service
cloud:
alibaba:
seata:
tx-service-group: minbox-seata
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
eureka:
client:
service-url:
defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
server:
port: 8081
複製代碼
導入Seata數據源代理配置
/** * @author 恆宇少年 */
@SpringBootApplication
@Import(DataSourceProxyAutoConfiguration.class)
public class GoodServiceApplication {
/** * logger instance */
static Logger logger = LoggerFactory.getLogger(GoodServiceApplication.class);
public static void main(String[] args) {
SpringApplication.run(GoodServiceApplication.class, args);
logger.info("商品服務啓動成功.");
}
}
複製代碼
服務接口
訂單服務
提供了下單的接口,經過調用該接口完成下單功能,下單接口會經過Openfeign
調用account-service
、good-service
所提供的服務接口來完成數據驗證,以下所示:
/** * @author 恆宇少年 */
@RestController
@RequestMapping(value = "/order")
public class OrderController {
/** * 帳戶服務接口 */
@Autowired
private AccountClient accountClient;
/** * 商品服務接口 */
@Autowired
private GoodClient goodClient;
/** * 訂單業務邏輯 */
@Autowired
private OrderService orderService;
/** * 經過{@link GoodClient#reduceStock(Integer, int)}方法減小商品的庫存,判斷庫存剩餘數量 * 經過{@link AccountClient#deduction(Integer, Double)}方法扣除商品所須要的金額,金額不足由account-service拋出異常 * * @param goodId {@link Good#getId()} * @param accountId {@link Account#getId()} * @param buyCount 購買數量 * @return */
@PostMapping
@GlobalTransactional
public String submitOrder( @RequestParam("goodId") Integer goodId, @RequestParam("accountId") Integer accountId, @RequestParam("buyCount") int buyCount) {
Good good = goodClient.findById(goodId);
Double orderPrice = buyCount * good.getPrice();
goodClient.reduceStock(goodId, buyCount);
accountClient.deduction(accountId, orderPrice);
Order order = toOrder(goodId, accountId, orderPrice);
orderService.addOrder(order);
return "下單成功.";
}
private Order toOrder(Integer goodId, Integer accountId, Double orderPrice) {
Order order = new Order();
order.setGoodId(goodId);
order.setAccountId(accountId);
order.setPrice(orderPrice);
return order;
}
}
複製代碼
服務配置(application.yml)
spring:
application:
name: order-service
cloud:
alibaba:
seata:
tx-service-group: minbox-seata
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
eureka:
client:
service-url:
defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
server:
port: 8082
複製代碼
啓用Openfeign & 導入Seata數據源代理配置
/** * @author 恆宇少年 */
@SpringBootApplication
@EnableFeignClients(basePackages = "org.minbox.chapter.seata.openfeign")
@Import(DataSourceProxyAutoConfiguration.class)
public class OrderServiceApplication {
/** * logger instance */
static Logger logger = LoggerFactory.getLogger(OrderServiceApplication.class);
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
logger.info("訂單服務啓動成功.");
}
}
複製代碼
咱們僅在order-service
調用了其餘服務的Openfeign
接口,因此咱們只須要在order-service
內經過@EnableFeignClients
註解啓用Openfeign
接口實現代理。
服務想要鏈接到Seata Server
須要添加兩個配置文件,分別是registry.conf
、file.conf
。
registry.conf
註冊到Seata Server
的配置文件,裏面包含了註冊方式、配置文件讀取方式,內容以下所示:
registry {
# file、nacos、eureka、redis、zk、consul
type = "file"
file {
name = "file.conf"
}
}
config {
type = "file"
file {
name = "file.conf"
}
}
複製代碼
file.conf
該配置文件內包含了使用file
方式鏈接到Eureka Server
的配置信息以及存儲分佈式事務信息
的方式,以下所示:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
}
## transaction log store
store {
## store mode: file、db
mode = "file"
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
datasource = "druid"
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://10.180.98.83:3306/iot-transactional"
user = "dev"
password = "dev2019."
}
}
service {
vgroup_mapping.minbox-seata = "default"
default.grouplist = "10.180.98.83:8091"
enableDegrade = false
disable = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
}
複製代碼
配置文件內service
部分須要注意,咱們在application.yml
配置文件內配置了事務分組爲minbox-seata
,在這裏須要進行對應配置vgroup_mapping.minbox-seata = "default"
,經過default.grouplist = "10.180.98.83:8091"
配置Seata Server
的服務列表。
將上面兩個配置文件在各個服務
resources
目錄下建立。
在前面說了那麼多,只是作了準備工做,咱們要爲每一個參與下單的服務添加對應的業務邏輯。
帳戶服務
在account-service
內添加帳戶餘額扣除業務邏輯類,AccountService
以下所示:
/** * 帳戶業務邏輯處理 * * @author 恆宇少年 */
@Service
@Transactional(rollbackFor = Exception.class)
public class AccountService {
@Autowired
private EnhanceMapper<Account, Integer> mapper;
/** * {@link EnhanceMapper} 具體使用查看ApiBoot官網文檔http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html * * @param accountId {@link Account#getId()} * @param money 扣除的金額 */
public void deduction(Integer accountId, Double money) {
Account account = mapper.selectOne(accountId);
if (ObjectUtils.isEmpty(account)) {
throw new RuntimeException("帳戶:" + accountId + ",不存在.");
}
if (account.getMoney() - money < 0) {
throw new RuntimeException("帳戶:" + accountId + ",餘額不足.");
}
account.setMoney(account.getMoney().doubleValue() - money);
mapper.update(account);
}
}
複製代碼
商品服務
在good-service
內添加查詢商品、扣減商品庫存的邏輯類,GoodService
以下所示:
/** * 商品業務邏輯實現 * * @author 恆宇少年 */
@Service
@Transactional(rollbackFor = Exception.class)
public class GoodService {
@Autowired
private EnhanceMapper<Good, Integer> mapper;
/** * 查詢商品詳情 * * @param goodId {@link Good#getId()} * @return {@link Good} */
public Good findById(Integer goodId) {
return mapper.selectOne(goodId);
}
/** * {@link EnhanceMapper} 具體使用查看ApiBoot官網文檔http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html * 扣除商品庫存 * * @param goodId {@link Good#getId()} * @param stock 扣除的庫存數量 */
public void reduceStock(Integer goodId, int stock) {
Good good = mapper.selectOne(goodId);
if (ObjectUtils.isEmpty(good)) {
throw new RuntimeException("商品:" + goodId + ",不存在.");
}
if (good.getStock() - stock < 0) {
throw new RuntimeException("商品:" + goodId + "庫存不足.");
}
good.setStock(good.getStock() - stock);
mapper.update(good);
}
}
複製代碼
咱們在執行測試以前在數據庫內的seata_account
、seata_good
表內對應添加兩條測試數據,以下所示:
-- seata_good
INSERT INTO `seata_good` VALUES (1,'華爲Meta 30',10,5000.00);
-- seata_account
INSERT INTO `seata_account` VALUES (1,10000.00,'2019-10-11 02:37:35',NULL);
複製代碼
將咱們本章所使用good-server
、order-service
、account-service
三個服務啓動。
咱們添加的帳戶餘額測試數據夠咱們購買兩件商品,咱們先來購買一件商品驗證下接口訪問是否成功,經過以下命令訪問下單接口:
~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=1
下單成功.
複製代碼
經過咱們訪問/order
下單接口,根據響應的內容咱們肯定商品已經購買成功。
經過查看order-service
控制檯內容:
2019-10-11 16:52:15.477 INFO 13142 --- [nio-8082-exec-4] i.seata.tm.api.DefaultGlobalTransaction : [10.180.98.83:8091:2024417333] commit status:Committed
2019-10-11 16:52:16.412 INFO 13142 --- [atch_RMROLE_2_8] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=10.180.98.83:8091:2024417333,branchId=2024417341,branchType=AT,resourceId=jdbc:mysql://localhost:3306/test,applicationData=null
2019-10-11 16:52:16.412 INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler : Branch committing: 10.180.98.83:8091:2024417333 2024417341 jdbc:mysql://localhost:3306/test null
2019-10-11 16:52:16.412 INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
複製代碼
咱們能夠看到本次事務已經成功Committed
。
再去驗證下數據庫內的帳戶餘額
、商品庫存
是否有所扣減。
測試商品添加了10
個庫存,在以前測試已經銷售掉了一件商品,咱們測試購買數量超過庫存數量時,是否有回滾日誌,執行以下命令:
~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=10
{"timestamp":"2019-10-11T08:57:13.775+0000","status":500,"error":"Internal Server Error","message":"status 500 reading GoodClient#reduceStock(Integer,int)","path":"/order"}
複製代碼
在咱們good-service
服務控制檯已經打印了商品庫存不足的異常信息:
java.lang.RuntimeException: 商品:1庫存不足.
at org.minbox.chapter.seata.service.GoodService.reduceStock(GoodService.java:42) ~[classes/:na]
....
複製代碼
咱們再看order-service
的控制檯打印日誌:
Begin new global transaction [10.180.98.83:8091:2024417350]
2019-10-11 16:57:13.771 INFO 13142 --- [nio-8082-exec-5] i.seata.tm.api.DefaultGlobalTransaction : [10.180.98.83:8091:2024417350] rollback status:Rollbacked
複製代碼
經過日誌能夠查看本次事務進行了回滾
。
因爲庫存的驗證在帳戶餘額扣減以前,因此咱們本次並不能從數據庫的數據來判斷事務是真的回滾。
既然商品庫存不足咱們不能直接驗證數據庫事務回滾,咱們從帳戶餘額不足來下手,在以前成功購買了一件商品,帳戶的餘額還夠購買一件商品,商品庫存目前是9件
,咱們本次測試購買5件
商品,這樣就會出現購買商品庫存充足
而餘額不足
的應用場景,執行以下命令發起請求:
~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=5
{"timestamp":"2019-10-11T09:03:00.794+0000","status":500,"error":"Internal Server Error","message":"status 500 reading AccountClient#deduction(Integer,Double)","path":"/order"}
複製代碼
咱們經過查看account-service
控制檯日誌能夠看到:
java.lang.RuntimeException: 帳戶:1,餘額不足.
at org.minbox.chapter.seata.service.AccountService.deduction(AccountService.java:33) ~[classes/:na]
複製代碼
已經拋出了餘額不足
的異常。
經過查看good-service
、order-serivce
控制檯日誌,能夠看到事務進行了回滾操做。
接下來查看seata_account
表數據,咱們發現帳戶餘額沒有改變,帳戶服務的事務回滾
驗證成功。
查看seata_good
表數據,咱們發現商品的庫存也沒有改變,商品服務的事務回滾
驗證成功。
本章主要來驗證分佈式事務框架Seata
在MySQL
下提交與回滾有效性,是否可以完成咱們預期的效果,Seata
做爲SpringCloud Alibaba
的核心框架,更新頻率比較高,快速的解決使用過程當中遇到的問題,是一個潛力股,不錯的選擇。
因爲本章設計的代碼比較多,請結合源碼進行學習。
請訪問gitee.com/hengboy/spr…查看本章源碼,建議使用git clone https://gitee.com/hengboy/spring-cloud-chapter.git
將源碼下載到本地。
本文由博客一文多發平臺 OpenWrite 發佈!