本系列筆記涉及到的代碼在GitHub上冻璃,地址:https://github.com/zsllsz/cloud
本文涉及知識點:
- 分布式事務解決方案之Alibaba seata;
一芙沥、分布式事務問題
打個比方,我們在淘寶下單買一件商品烤黍,可能涉及到了三個微服務魂爪,分別是訂單服務蝌数,庫存服務,支付服務贷祈。這三個服務連接的是三個不同的數(shù)據(jù)庫趋急,但是下單的這一個過程對外是表現(xiàn)出一個整體。比如下單成功势誊,然后扣庫存也成功了呜达,最后支付這一步失敗了,那么庫存系統(tǒng)和訂單系統(tǒng)都應該回滾這一次操作粟耻。同一個數(shù)據(jù)庫用事務就可以回滾了查近,不同數(shù)據(jù)庫,那就要用分布式事務了挤忙。即每個數(shù)據(jù)庫內(nèi)部的數(shù)據(jù)一致性由本地事務來保證霜威,全局數(shù)據(jù)一致性就由分布式事務來保證。
歡迎大家關注我的公眾號 javawebkf饭玲,目前正在慢慢地將簡書文章搬到公眾號侥祭,以后簡書和公眾號文章將同步更新叁执,且簡書上的付費文章在公眾號上將免費茄厘。
二、springCloud Alibaba Seata簡介
1谈宛、是什么次哈?
seata就是用來解決分布式事務的。官網(wǎng)地址:http://seata.io/zh-cn/
2吆录、seata相關術語:
分布式事務處理過程的1個id + 3個組件
模型:1個id就是指全局唯一的事務id(transaction id)窑滞;3個組件指的是:
Transaction Coordinator(TC):事務協(xié)調(diào)者,說白了就是你服務器上安裝的seata恢筝。維護全局和分支事務的狀態(tài)哀卫,驅(qū)動全局事務提交或回滾。
Transaction Manager(TM):事務管理者撬槽,說白了就是你加了@GlobalTransactional注解的那個方法此改。定義全局事務的范圍,負責開啟一個全局事務侄柔,并最終發(fā)起全局事務提交或者回滾的決議共啃。
Resource Manager(RM):資源管理器,說白了就是本次涉及到的數(shù)據(jù)庫暂题。管理分支事務的資源移剪,負責分支注冊、狀態(tài)匯報薪者,并接收事務協(xié)調(diào)器的指令纵苛,驅(qū)動分支(本地)事務的提交和回滾。
3、seata處理分布式事務的過程:
- TM向TC申請開啟一個全局事務攻人,全局事務創(chuàng)建成功并生成一個全局唯一的XID幔虏;
- XID在微服務調(diào)用鏈路的上下文中傳播;
- RM向TC注冊分支事務贝椿,將其納入XID對用的全局事務的管轄想括;
- TM向TC發(fā)起針對XID的全局提交或回滾決議;
- TC調(diào)度XID下管轄的全部分支事務完成提交或回滾請求烙博。
簡單地說瑟蜈,整個過程就是用一個XID關聯(lián)起來的,比如下訂單的過程是一個整體過程渣窜,需要用分布式事務铺根,那么訂單系統(tǒng)、庫存系統(tǒng)和支付系統(tǒng)就會被同一個XID管著乔宿,表明它們是一個整體位迂。每個系統(tǒng)就是一個RM,每個系統(tǒng)自己的事務由本地事務完成详瑞,每個系統(tǒng)的操作提交還是回滾都會告訴TM掂林,TM再把結(jié)果告訴最終該提交還是回滾告訴TC去執(zhí)行。
3坝橡、怎么玩泻帮?
- 去哪兒下:https://github.com/seata/seata/tags
- 怎么用:本地加@Transactional注解,全局加@GlobalTrasactional注解就完事了(先有個映像计寇,編碼實戰(zhàn)部分再看具體用法)
- 安裝:官網(wǎng)下載后锣杂,解壓
- 在seata/config目錄下,有一個nacos-config.txt番宁,打開它元莫,關注文件中的
service.vgroup_mapping.my_test_tx_group=default
my_test_tx_group
就是組名,等下在file.conf和項目的application.yml中都要用到的蝶押。
- 修改conf目錄下的file.conf踱蠢,主要改的是自定義事務組名稱、事務日志存儲模式改為db播聪、數(shù)據(jù)庫連接信息朽基,如下:
這一段是修改事務組名稱,即修改了service塊的第一行离陶,注意第一行要跟nacos-config.txt中的那一行對應稼虎,說白了就是將nacos-config.txt中的那一行拷貝過來去掉service.
,等于號后面的值用引號引起來就可以了招刨。還有就是default.grouplist后面的ip和端口霎俩,就是你seata啟動的ip和端口。
service {
#vgroup->rgroup
vgroup_mapping.my_test_tx_group = "default"
#only support single node
default.grouplist = "192.168.0.106: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"
}
store塊,存儲模式由file改為db打却。
store {
## store mode: file杉适、db
mode = "db"
……
}
db塊中配置自己的數(shù)據(jù)庫連接信息。
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://192.168.0.106:3306/seata"
user = "root"
password = "zsl"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
- 新建數(shù)據(jù)庫seata柳击;
- 在seata里新建表猿推,建表的sql在conf目錄下,名為db_store.sql捌肴,在seata庫執(zhí)行就好了蹬叭;
- 修改conf目錄下的registry.conf,指明注冊中心為nacos状知,配置nacos的連接信息秽五,如下:
registry {
# file 、nacos 饥悴、eureka坦喘、redis、zk西设、consul瓣铣、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "192.168.0.106:8848"
namespace = ""
cluster = "default"
}
- 啟動nacos济榨;
- 初始化seata的nacos配置:進入seata/conf目錄坯沪,執(zhí)行:
sh nacos-config.sh 192.168.0.106
這個ip就是你nacos所在的服務器IP绿映。
- 啟動seata-server擒滑,直接執(zhí)行seata/bin目錄執(zhí)行:
sh seata-server.sh -p 8091 -m db
-p是端口,-m是存儲模式叉弦,我們配置了db存儲丐一,所以這里用db。
最后日志打印出如下日志表明啟動成功:
load RegistryProvider[Nacos] extension by class[io.seata.discovery.registry.nacos.NacosRegistryProvider]
三淹冰、實戰(zhàn)之數(shù)據(jù)庫的準備
創(chuàng)建三個微服務库车,調(diào)用鏈路為 下訂單 ---> 扣庫存 ---> 減余額。
1樱拴、創(chuàng)建數(shù)據(jù)庫:
- seata_order:存儲訂單信息的數(shù)據(jù)庫
- seata_storage:存儲庫存信息的數(shù)據(jù)庫
- seata_account:存儲賬戶信息的數(shù)據(jù)庫
建庫的sql如下:
create database seata_order;
create database seata_storage;
create database seata_account;
2柠衍、建立業(yè)務數(shù)據(jù)表:
- seata_order庫下新建t_order表:
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產(chǎn)品id',
`count` int(11) DEFAULT NULL COMMENT '數(shù)量',
`money` decimal(11, 0) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '訂單狀態(tài): 0:創(chuàng)建中 1:已完結(jié)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '訂單表' ROW_FORMAT = Dynamic;
- seata_storage庫下新建t_storage表:
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '產(chǎn)品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '庫存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
- seata_account庫下新建t_account表:
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL 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 NULL COMMENT '剩余可用額度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '賬戶表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
3、新建事務回滾日志表:
上面新建的三個數(shù)據(jù)庫都需要新建各自的回滾日志表晶乔。在三個業(yè)務數(shù)據(jù)庫中都執(zhí)行seata-server-0.9.0\seata\conf\
目錄下的db_undo_log.sql
即可珍坊。
四、實戰(zhàn)之業(yè)務代碼的編寫
業(yè)務需求:下訂單 ---> 減庫存 ---> 扣余額 ---> 改訂單狀態(tài)正罢。
1阵漏、新建訂單模塊seata-order-service2001:
- pom.xml:主要是nacos、seata、openfeign和數(shù)據(jù)庫連接那一套履怯。
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos -->
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!-- seata -->
<!--feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<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>
</dependency>
<!-- druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- application.yml(主要是要注意tx-service-group的值要與nacos-config.txt和file.conf中的對應):
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定義事務組名稱需要與seata-server中的對應
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: 192.168.0.106:8848
datasource:
# 當前數(shù)據(jù)源操作類型
type: com.alibaba.druid.pool.DruidDataSource
# mysql驅(qū)動類
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.0.106:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: zsl
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath:mapper/*.xml
- seata配置文件:將seata/conf下的file.conf和registry.conf拷貝到application.yml的同級目錄下回还。
- CommonResult.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
}
- Order.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; // 0:創(chuàng)建中, 1:已完結(jié)
}
- OrderDao.java:
@Mapper
public interface OrderDao {
/** 創(chuàng)建訂單 */
public void create(Order order);
/** 修改訂單狀態(tài) */
public void update(@Param("userId") Long userId, @Param("status") Integer status);
}
- OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhusl.springcloud.dao.OrderDao">
<insert id="create" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
insert into t_order(user_id, product_id, count, money, status)
values(#{userId}, #{productId}, #{count}, #{money}, 0);
</insert>
<update id="update">
update t_order set status = 1 where user_id = #{userId} and status = #{status};
</update>
</mapper>
- OrderService.java:
public interface OrderService {
/** 創(chuàng)建訂單 */
public void create(Order order);
}
- OrderServiceImpl.java:
@Service
@Slf4j
public class OrderServiceImpl implements OrderService{
@Autowired
private OrderDao orderDao;
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
@Override
public void create(Order order) {
log.info("================= 新建訂單start ==============");
orderDao.create(order);
log.info("================= 新建訂單end ==============");
log.info("================= 訂單微服務調(diào)用庫存微服務扣減庫存start ==============");
storageService.decrease(order.getProductId(), order.getCount());
log.info("================= 訂單微服務調(diào)用庫存微服務扣減庫存end ==============");
log.info("================= 訂單微服務調(diào)用賬戶微服務做扣減余額start ==============");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("================= 訂單微服務調(diào)用賬戶微服務做扣減余額end ==============");
log.info("================= 修改訂單狀態(tài)start ==============");
orderDao.update(order.getUserId(), 0);
log.info("================= 修改訂單狀態(tài)end ==============");
log.info("================= 下單完成? ==============");
}
}
- StorageService.java:
@FeignClient(value = "seata-storage-service")
public interface StorageService {
/** 扣減庫存 */
@PostMapping("/storage/decrease")
public CommonResult<?> decrease(@RequestParam("productId") Long id, @RequestParam("count") Integer count);
}
- AccountService.java:
@FeignClient(value = "seata-account-service")
public interface AccountService {
/** 扣余額 */
@PostMapping("/account/decrease")
public CommonResult<?> decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
- OrderController.java:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/create")
public CommonResult<?> create(Order order) {
orderService.create(order);
return new CommonResult<>(200, "訂單創(chuàng)建成功", null);
}
}
- DataSourceProxyConfig.java:
package com.zhusl.springcloud.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
/**
* 使用seata對數(shù)據(jù)源進行代理
* @author zhu
*
*/
@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();
}
}
- 主啟動類:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消數(shù)據(jù)源的自動創(chuàng)建叹洲,用自己配置的
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan({"com.zhusl.springcloud.dao"})
public class App {
public static void main(String[] args) throws Exception {
SpringApplication.run(App.class, args);
}
}
準備完成柠硕,現(xiàn)在依次啟動nacos、seata和2001這個項目运提,最后項目控制臺打印出如下日志并且在nacos中有seata和2001這兩個服務表明啟動成功仅叫。
啟動成功的日志:
2020-06-05 11:29:12.393 INFO 74764 --- [ main] com.zhusl.springcloud.App : Started App in 7.465 seconds (JVM running for 7.931)
2020-06-05 11:29:12.676 INFO 74764 --- [imeoutChecker_1] i.s.c.r.netty.NettyClientChannelManager : will connect to 192.168.2.43:8091
2020-06-05 11:29:12.676 INFO 74764 --- [imeoutChecker_1] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:TMROLE,address:192.168.2.43:8091,msg:< RegisterTMRequest{applicationId='seata-order-service', transactionServiceGroup='my_test_tx_group'} >
2020-06-05 11:29:12.751 INFO 74764 --- [imeoutChecker_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 71 ms, version:0.9.0,role:TMROLE,channel:[id: 0x9fe21753, L:/192.168.2.36:65186 - R:/192.168.2.43:8091]
在配置啟動過程中遇到了很多問題,大家可以去官網(wǎng)尋找解決方案糙捺。遇事不要慌诫咱,官網(wǎng)來幫忙。
https://github.com/seata/seata-samples
2洪灯、新建名為seata-storage-service2002的庫存module:
- pom.xml:和2001訂單module的一模一樣坎缭;
- application.yml:端口改成2002,微服務名稱改成seata-storage-service签钩,連接的數(shù)據(jù)庫改成seata_storage掏呼,其他的都和2001的一樣;
- 把file.conf和registry.conf復制粘貼到application.yml的同級目錄下铅檩;
- Storage.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
/** 產(chǎn)品id */
private Long productId;
/** 總庫存 */
private Integer total;
/** 已用庫存 */
private Integer used;
/** 剩余庫存 */
private Integer residue;
}
- StorageDao.java:
@Mapper
public interface StorageDao {
/** 扣減庫存 */
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
- StorageMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhusl.springcloud.dao.StorageDao">
<update id="decrease">
update t_storage set used = used + #{count}, residue = residue - #{count} where product_id = #{productId};
</update>
</mapper>
- StorageServiceImpl.java:
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageDao storageDao;
@Override
public void decrease(Long productId, Integer count) {
log.info("============== storageService 扣減庫存 start =============");
storageDao.decrease(productId, count);
log.info("============== storageService 扣減庫存 end =============");
}
}
- StorageController.java:
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
private StorageService storageService;
@PostMapping("/decrease")
public CommonResult<?> decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult<>(200, "扣減庫存成功憎夷!", null);
}
}
- 最后數(shù)據(jù)源的配置和主啟動類和都和2001的一樣,復制粘貼即可昧旨。
3拾给、新建名為seata-account-service2003的賬戶module:
- pom.xml:和2001的一模一樣;
- application.yml:端口改為2003兔沃,服務名改成seata-account-service蒋得,連接的數(shù)據(jù)庫改成seata_account;
- 復制粘貼file.conf和registry.conf到application.yml的同級目錄乒疏;
- Account.java:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
/** 用戶id */
private Long userId;
/** 總額度 */
private BigDecimal total;
/** 已用額度 */
private BigDecimal used;
/** 剩余額度 */
private BigDecimal residue;
}
- AccountDao.java:
@Mapper
public interface AccountDao {
/** 扣減余額 */
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
- AccountMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhusl.springcloud.dao.AccountDao">
<update id="decrease">
update t_account set residue = residue - #{money},used = used + #{money} where user_id = #{userId};
</update>
</mapper>
- AccountServiceImpl.java:
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("================ account-service 扣減余額 start ===============");
accountDao.decrease(userId, money);
log.info("================ account-service 扣減余額 end ===============");
}
}
- AccountController.java:
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/decrease")
public CommonResult<?> decrease(Long userId, BigDecimal money) {
accountService.decrease(userId, money);
return new CommonResult<>(200, "扣減余額成功!", null);
}
}
- 最后別忘記主啟動類和數(shù)據(jù)源配置類额衙。
4、測試:
3個module建完怕吴,先測試一下能否成功運行起來窍侧,先啟動nacos,再啟動seata转绷,然后依次啟動3個module伟件。下面是3張表的初始情況:
現(xiàn)在模擬正常下單:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
訪問之后,可能出現(xiàn)兩種情況:
- 返回成功信息暇咆,數(shù)據(jù)庫成功的創(chuàng)建了一條訂單锋爪,account和storage也成功的扣除了對應的數(shù)量丙曙。
- openfeign報錯,read timeout其骄,成功創(chuàng)建了訂單亏镰,但是account沒有扣減。
如果出現(xiàn)第二種情況拯爽,那也充分說明了目前這三個操作沒有在一個事務里索抓。如果你想不報錯,不想讓openfeign超時毯炮,加上在application.yml中加上如下配置即可:
ribbon:
ReadTimeout: 10000 #10秒應該就不會超時了
ConnectTimeout: 10000
接下來我們在account的service里讓線程睡11秒鐘逼肯,雖然剛才openfeign設置了超時時間10秒,但是現(xiàn)在睡11秒桃煎,肯定還是會異常的篮幢。然后在OrderServiceImpl類上加上全局事務注解:
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
name隨意,不沖突就好为迈,rollbackFor表示什么情況下回滾三椿,這里的意思是報異常了就回滾。
配置好之后葫辐,重新啟動account和order這兩個微服務搜锰,最后再次訪問下訂單的鏈接。就會發(fā)現(xiàn)報超時異常了耿战,但是三個數(shù)據(jù)庫的三張表都沒有數(shù)據(jù)變化蛋叼,即使全部都回滾了,這就表明分布式事務起作用了剂陡。
五狈涮、關于seata的其他說明
seata官網(wǎng)上說它支持AT、TCC鹏倘、SAGA 和 XA 事務模式薯嗤,我們用的是默認的AT模式。
1纤泵、AT模式如何做到對業(yè)務無侵入的?
- AT模式的前提:基于支持ACID事務的關系型數(shù)據(jù)庫镜粤,通過JDBC訪問數(shù)據(jù)庫的java應用捏题;
- 整體機制:兩階段提交協(xié)議。
- 一階段:seata會攔截業(yè)務sql肉渴,找到業(yè)務要更新的數(shù)據(jù)公荧,在被更新之前,將其保存為before image同规;執(zhí)行業(yè)務sql循狰,更新業(yè)務數(shù)據(jù)窟社;在業(yè)務數(shù)據(jù)更新之后,將其保存為after image绪钥,最后生成行鎖灿里。一階段的操作都在一個數(shù)據(jù)庫事務內(nèi)完成,保證了一階段操作的原子性程腹。這就類似spring的aop思想匣吊,前置通知和后置通知。
- 二階段提交:如果順利寸潦,二階段就進行提交色鸳。因為一階段已經(jīng)執(zhí)行過業(yè)務sql了,所以這里只需要將一階段保存的before image见转、after image和行鎖刪除即可命雀。
- 二階段回滾:如果出異常了需要回滾,通過一階段的回滾日志進行反向補償斩箫。首先會比較當前庫中數(shù)據(jù)和after image是否一致咏雌,如果一致,那么就將數(shù)據(jù)庫中的數(shù)據(jù)還原成before image校焦;如果不一致赊抖,說明數(shù)據(jù)出現(xiàn)過臟寫,需要人工處理寨典。