目录
八、Seata的XA模式
8.1 XA原理流程图
8.2 流程图详细说明
8.3 XA模式多数据源场景
1 环境搭建
-
建库建表
代码的db.sql中 -
创建工程
-
添加依赖
<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.2.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
- 编写主配置类
@SpringBootApplication
public class XAMultiApp {
public static void main(String[] args) {
SpringApplication.run(XAMultiApp.class);
}
}
- 配置文件
server:
port: 8080
spring:
datasource:
nong:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.239.11:3306/nonghang?serverTimezone=UTC
username: root
password: houchen
jian:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.239.11:3306/jianhang?serverTimezone=UTC
username: root
password: houchen
logging:
level:
com:
hc: debug
-
mapper + 业务代码
见上述 gitee 仓库 -
编写多数据源配置
@SpringBootConfiguration
@MapperScans({@MapperScan(basePackages = "com.hc.nong", sqlSessionFactoryRef = "nongFactory"), @MapperScan(basePackages = "com.hc.jian", sqlSessionFactoryRef = "jianFactory")})
public class DbConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.nong")
public DruidDataSource nongDataSource() {
return new DruidDataSource();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.jian")
public DruidDataSource jianDataSource() {
return new DruidDataSource();
}
/**
* 配置NongSqlSession
*/
@Bean
public SqlSessionFactory nongFactory(@Qualifier("nongDataSource") DruidDataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionFactory jianFactory(@Qualifier("jianDataSource") DruidDataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean.getObject();
}
}
2、使⽤XA模式解决事务
- 添加依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
- yaml文件
注意:
我们添加的是seata-spring-boot-starter
依赖⽀持registry.conf和 file.conf 同时也⽀持把配置编写在application.yml中
但是:
当我们添加的是seata-all依赖时 我们需要添加registry.conf和 file.conf 不能使⽤yml格式配置
因为seata-all没有⾃动配置 所以依赖的不同的包 配置是不同的
seata:
enabled: true
registry:
type: file
config:
type: file
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false #默认为false 可以不配置
application-id: abc # 初始化TM和RM使用
tx-service-group: default_tx_group
enable-auto-data-source-proxy: true # 设置datasource自动代理
data-source-proxy-mode: XA # 指定代理模式 XA
-
⾃动代理的⽅式
就是上面yaml文件的最后两行配置 -
业务代码上面添加注解
@GlobalTransactional(rollbackFor = Exception.class)
public void transfer(int fromId, int toId, double monoey) {
nongMapper.reduceMoney(fromId, monoey);
int i = 10 / 0;
jianMapper.increaseMoney(toId, monoey);
}
- 测试
调用接口 http://localhost:8080/user/transfer?fromId=1&toId=1&monoey=100,查看日志
⼿动代理的⽅式
-
修改配置关闭⾃动代理
-
⼿动构建代理数据源
@SpringBootConfiguration
@MapperScans({@MapperScan(basePackages = "com.hc.nong", sqlSessionFactoryRef = "nongFactory"), @MapperScan(basePackages = "com.hc.jian", sqlSessionFactoryRef = "jianFactory")})
public class DbConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.nong")
public DruidDataSource nongDataSource() {
return new DruidDataSource();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.jian")
public DruidDataSource jianDataSource() {
return new DruidDataSource();
}
/**
* ⼿动配置农⾏代理数据源
* @param dataSource
* @return
*/
@Bean
public DataSourceProxyXA nongXA(@Qualifier("nongDataSource") DruidDataSource dataSource){
return new DataSourceProxyXA(dataSource);
}
/**
* ⼿动代理建⾏数据源
* @param dataSource
* @return
*/
@Bean
public DataSourceProxyXA jianXA(@Qualifier("jianDataSource") DruidDataSource dataSource){
return new DataSourceProxyXA(dataSource);
}
/**
* 配置NongSqlSession
*/
/*@Bean
public SqlSessionFactory nongFactory(@Qualifier("nongDataSource") DruidDataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionFactory jianFactory(@Qualifier("jianDataSource") DruidDataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean.getObject();
}*/
/**
* 配置NongSqlSession
*/
@Bean
public SqlSessionFactory nongFactory(@Qualifier("nongXA") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionFactory jianFactory(@Qualifier("jianXA") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean.getObject();
}
}
8.4 XA模式分库分表场景
添加依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
<version>4.1.1</version>
</dependency>
添加注解
@GetMapping("/transfer")
@ShardingTransactionType(TransactionType.XA)
@Transactional
public String transfer(Long fromId, Long toId, double money) {
User fromUser = userMapper.findById(fromId);
User toUser = userMapper.findById(toId);
fromUser.setMoney(fromUser.getMoney() - money);
toUser.setMoney(toUser.getMoney() + money);
userMapper.updateUser(fromUser);
int i = 1 / 0;
userMapper.updateUser(toUser);
return "success";
}
8.5 XA模式微服务场景
1、环境搭建
创建订单 —> 扣库存 —> 扣减余额
具体见 gitee : https://gitee.com/houchen1996/seata-shangma
xa-cloud-master
2、XA默认解决事务问题
1) 通⽤⼯程添加依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
- 所有⼯程添加配置
seata:
service:
vgroup-mapping:
shangma: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false #默认为false 可以不配置
tx-service-group: shangma
enable-auto-data-source-proxy: true
data-source-proxy-mode: XA
3) 业务系统修改
@SpringBootApplication(exclude = SeataTCCFenceAutoConfiguration.class)
// 新版本的seata tcc模式需要数据源,而业务系统并没有配置数据源
@SpringBootApplication(exclude = SeataTCCFenceAutoConfiguration.class)
public class XABusinessApp {
public static void main(String[] args) {
SpringApplication.run(XABusinessApp.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
interceptors.add(new XIDIntercepter());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
4)业务系统添加 @GlobalTransactional
@Service
@Slf4j
public class BusinessService {
@Autowired
RestTemplate restTemplate;
@GlobalTransactional
public void placeOrder(int accountId, int goodId, int num) {
log.info("order是否在事务中:{}", RootContext.inGlobalTransaction());
log.info("全局事务ID:{}", RootContext.getXID());
log.info("事务模式:{}", RootContext.getBranchType());
Good good = restTemplate.getForObject(String.format(Urls.GOOD_INFO, goodId), Good.class);
double amount = good.getGoodPrice() * num;
Order order = new Order().setGoodId(goodId).setGoodNum(num).setAccountId(accountId).setOrderAmount(amount).setStatus(OrderStatus.CREATE.getValue());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders);
// 创建订单
restTemplate.postForObject(Urls.CEATE_ORDER, httpEntity, String.class);
// 减库存
restTemplate.getForObject(String.format(Urls.REDUCE_STOCK, goodId, num), Object.class);
// 减余额
restTemplate.getForObject(String.format(Urls.REDUCE_MONEY, accountId, amount), Object.class);
}
}
5)测试问题分析
微服务下,事务失效的两个原因:
-
使⽤⾃带的HikariDataSource数据源 切换XA时⽆效 通过打印查看还是默认的AT (兼容问题)
-
在业务系统开启全局事务 全局事务id返回给了业务系统,但是订单 账户 商品服务想要注册分⽀事
务 需要全局事务id 此时全局事务id需要从业务系统传递给订单 账户 商品服务 此时各个服务才能
注册分⽀事务
3、全局事务id传递问题解决
1) 业务系统定义http请求拦截器
public class XIDIntercepter implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
if (StringUtils.isNotBlank(RootContext.getXID())) {
httpRequest.getHeaders().add(RootContext.KEY_XID, RootContext.getXID());
}
return clientHttpRequestExecution.execute(httpRequest, bytes);
}
}
2) 业务系统http请求设置拦截器
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
interceptors.add(new XIDIntercepter());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
- common模块添加添加拦截器
@Component
public class ReceiveXIDInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String xid = request.getHeader(RootContext.KEY_XID);
if (StringUtils.isNotBlank(xid)) {
RootContext.bind(xid);
}
return true;
}
}
请求: http://localhost:8103/placeOrder?accountId=1&goodId=1&num=100
4、数据源兼容问题解决 - 使用durid数据源
1) common模块添加依赖
// 为了引入 PlatformTransactionManager 类
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<optional>true</optional>
</dependency>
- common工程添加配置类
@Configuration
@ConditionalOnClass(PlatformTransactionManager.class)
public class DbConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
}
5、使⽤Cloud包优化
刚刚我们添加的是Seata的boot下的依赖 此时我们在微服务项⽬中 想要让事务⽣效,需要⼿动传递事务id
如果调⽤的服务⽐较多 调⽤链路⽐较复杂 每次都要⼿动传递事务id 很显然不够优雅 此时我们可以使⽤springCloud提供的seata包 来优化传递id 因为springCloud包 已经把传递事务id完成了 我们不需要关注事务ID的传递
并且在实际开发中 我们不仅可以使⽤Ribbon远程调⽤负载均衡也可以使⽤OpenFeign调⽤
改造项⽬ 使⽤cloud包 以及OpenFeign远程调⽤
见 gitee : https://gitee.com/houchen1996/seata-shangma
6、异常处理事务失效
以上的案例 之所以事务会回滚 是因为代码出现异常 TM通知TC让RM回滚事务
如果代码使⽤全局异常处理不让代码出异常,则seata是⽆法感知我们项⽬是出错的 此时全局事务就会失效
解决⽅式
⽅式⼀: 底层的各个服务异常不处理,只需要在业务系统处理异常,因为业务系统⾯向的是⽤户,所以不能看到错误⻚⾯
⽅式⼆: 在全局异常⾥⾯⼿动回滚
7、服务降级事务失效
1) 代码演示
- 通⽤⼯程添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 定义降级fallback类
@Component
public class AccountFeignClientFallback implements AccountFeignClient{
@Override
public Account findAccountById(int id) {
return null;
}
@Override
public String reduceMoney(int accountId, double money) {
System.out.println("reduceMoney 方法降级 !!!!");
return null;
}
}
- feign接口指定降级策略
@FeignClient(value = "seata-xa-account",fallback = AccountFeignClientFallback.class)
public interface AccountFeignClient {
@GetMapping("/account/{id}")
Account findAccountById(@PathVariable("id") int id);
@GetMapping("/account/reduceMoney")
String reduceMoney(@RequestParam("accountId")int accountId, @RequestParam("money")double money);
}
- 业务系统添加feign相关配置
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
sentinel:
enabled: true
- 测试
停止account服务,请求 http://localhost:8103/placeOrder?accountId=1&goodId=1&num=10
可以看到订单创建和库存扣减,但是账户余额没有扣减,分布式事务失效 。
2) ⼿动回滚
@Component
public class AccountFeignClientFallback implements AccountFeignClient{
@Override
public Account findAccountById(int id) {
return null;
}
@Override
public String reduceMoney(int accountId, double money) {
if(RootContext.inGlobalTransaction()) {
try {
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
} catch (TransactionException e) {
throw new RuntimeException(e);
}
}
System.out.println("reduceMoney 方法降级 !!!!");
return null;
}
}