一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
官网:http://seata.io/zh-cn/
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
AT 模式工作的两个阶段:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
一阶段加载
在一阶段, Seata会拦截 “业务SQL”:
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
二阶段回滚
update product set name = 'TXC' where id = 1;
下载:https://github.com/seata/seata/releases
开启全局事务只需要添加:@GlobalTransactional
第一步:修改配置文件
修改 conf 目录下的 file.conf 配置文件
【注】先备份再修改
主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息
service模块:
vgroup_mapping.my_test_tx_group = "zth_tx_group"
store模块:
mode = "db"
…………
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "mysql"
修改 /conf/registry.conf 配置文件
修改目的:指明注册中心为nacos,及修改nacos连接信息
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
第二步:mysql5.7新建库 seata 以及数据表
sql 脚本文件:seata\conf\db_store.sql
每个数据源中需添加回滚日志表:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
第三步:启动
先启动 nacos 在启动 seata。
启动方式:执行 \seata\bin\seata-server.bat
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
架构图
数据库:订单-库存-账户3个库以及 seata 库
seata-order-service2001
第一步:pom:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
seata-all
io.seata
io.seata
seata-all
0.9.0
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
5.1.37
com.alibaba
druid-spring-boot-starter
1.1.10
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
org.projectlombok
lombok
true
第二步:yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: zth_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: mysql
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
第三步:将 file.conf、registry.conf 复制到classpath下
第四步:domain
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
@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:创建中;1:已完结
}
第五步:Dao接口及实现
@Mapper
public interface OrderDao
{
//新建订单
void create(Order order);
//修改订单状态,从零改为1
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
resources下 mapper文件夹下:OrderMapper.xml
insert into t_order (id,user_id,product_id,count,money,status)
values (null,#{userId},#{productId},#{count},#{money},0);
update t_order set status = 1
where user_id=#{userId} and status = #{status};
第六步:Service接口及实现
OrderService:
public interface OrderService {
void create(Order order);
}
OrderServiceImpl
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
*/
@Override
@GlobalTransactional(name = "zth-create-order",rollbackFor = Exception.class)
public void create(Order order){
//新建订单
orderDao.create(order);
//扣减库存
storageService.decrease(order.getProductId(),order.getCount());
//扣减账户
accountService.decrease(order.getUserId(),order.getMoney());
//修改订单状态,从零到1代表已经完成
orderDao.update(order.getUserId(),0);
}
}
@GlobalTransactional(name = "zth-create-order",rollbackFor = Exception.class)
StorageService:
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
AccountService:
@FeignClient(value = "seata-account-service")
public interface AccountService{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
第七步:Controller
@RestController
public class OrderController{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
第八步:Config配置
MyBatisConfig
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
DataSourceProxyConfig
@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)//取消数据源自动创建的配置
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMainApp2001 {
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}