<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
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();
}
}
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.5.2version>
dependency>
但是:
当我们添加的是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);
}
⼿动代理的⽅式
@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();
}
}
添加依赖
<dependency>
<groupId>org.apache.shardingspheregroupId>
<artifactId>sharding-transaction-xa-coreartifactId>
<version>4.1.1version>
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";
}
创建订单 —> 扣库存 —> 扣减余额
具体见 gitee : https://gitee.com/houchen1996/seata-shangma
xa-cloud-master
1) 通⽤⼯程添加依赖
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
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需要从业务系统传递给订单 账户 商品服务 此时各个服务才能
注册分⽀事务
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;
}
@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
1) common模块添加依赖
// 为了引入 PlatformTransactionManager 类
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<optional>trueoptional>
dependency>
@Configuration
@ConditionalOnClass(PlatformTransactionManager.class)
public class DbConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
}
刚刚我们添加的是Seata的boot下的依赖 此时我们在微服务项⽬中 想要让事务⽣效,需要⼿动传递事务id
如果调⽤的服务⽐较多 调⽤链路⽐较复杂 每次都要⼿动传递事务id 很显然不够优雅 此时我们可以使⽤springCloud提供的seata包 来优化传递id 因为springCloud包 已经把传递事务id完成了 我们不需要关注事务ID的传递
并且在实际开发中 我们不仅可以使⽤Ribbon远程调⽤负载均衡也可以使⽤OpenFeign调⽤
改造项⽬ 使⽤cloud包 以及OpenFeign远程调⽤
见 gitee : https://gitee.com/houchen1996/seata-shangma
以上的案例 之所以事务会回滚 是因为代码出现异常 TM通知TC让RM回滚事务
如果代码使⽤全局异常处理不让代码出异常,则seata是⽆法感知我们项⽬是出错的 此时全局事务就会失效
解决⽅式
⽅式⼀: 底层的各个服务异常不处理,只需要在业务系统处理异常,因为业务系统⾯向的是⽤户,所以不能看到错误⻚⾯
1) 代码演示
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
@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;
}
}
@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:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
sentinel:
enabled: true
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;
}
}