SpringCloud Alibaba Seata
一次业务操作需要跨多个数据源或系统进行远程调用,就会产生分布式事务问题(即全局数据不统一)
此时可用Seata(网址: https://seata.io/zh-cn/)一款开源的分布式事务解决方案。
先下载seata-server-0.9.0.zip并解压
修改file-conf文件
#vgroup_mapping.my_test_tx_group = ""
vgroup_mapping.my_test_tx_group = "dio_order_tx_group"
#mode = "mode"
mode = "db" //日志存到数据库
#user = "mysql"
user = "root"
password = "xxx"
password ="123"
保存
创建数据库 "seata"
将seata文件夹下的db_store.sql所有语句复制到seata数据库即(lock_table,brach_table,global_table )三张表
修改seata文件夹下/conf/registry.conf配置注册中心
#type = "file"
type = "nacos"
#serverAddr = “localhost”
serverAddr = "localhost:8848"
先启动Nacos(8848)再打开seata/bin/seata-server.bat,启动Seata Server(默认8091)
例:使用seata ,order_service , account_service , storage_service 四张表完成库存业务,分别对应order,account,storage 3个模块
浏览器/客户端 微服务(order)
Seata Server
微服务(storage) 数据库
seata ,order_service,account_service , storage_service
微服务(account)
全局数据的一致性通过Seata Server 保证
创建好order,account,storage三库后,还要分别为他们创建回滚日志(seata/conf/db_undo_log.sql)
开发storage_service微服务模块
pom.xml文件:
新加Openfeign starter场景启动器
con.alibaba.cloud
spring-cloud-starter-openfeign
新加seata starter
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
<exclusion>
io.seata //排除自带的seata-all,引入自己的版本
seata-all
</exclusion>
io.seata
seata-all
0.9.0
application.yml
port:10010
name: seata_service
seata:
tx-service-group: order_group #指定事务组名,得喝conf/file.conf一致
sever-addr: localhost:8848 #注册到Nacos
datasource: #配置数据库
driver-class-name:...
url:...
mybatis:
mapperLocaltions: clsspath:mapper/*.xml #指定mybatis映射地址
创建file.conf在resources下,进行相关配置,该文件从 seata/conf/file.conf拷贝
vgroup_mapping.my_test_tx_group = "dio_order_tx_group"
改为
vgroup_mapping.dio_order_tx_group = "default"
创建registry.conf,同样从那里拷贝
开发实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage{
id,producetId,amount
}
Dao层
@Mapper
public interface StorageDao{
void reduce(@Param("productId") Long productId,
@Param("nums") Interger nums)
}
在resources下创建StorageMapper.xml来实现接口
...映射
<update id="reduce"> //实现方法
UPDATE
storage...
</update>
实现Service层
public interface StorageService{
void reduce(Long productId,Integer nums);
}
实现该接口
@Service
public class SrorageServiceImpl implements StorageService{
@Resource
private StorageDao storageDao;
@Override
reduce(..){
storageDao.reduce(productId,nums);
}
}
控制层Controller
@RestContoller
StorageController{
@Resource
private StorageService s..
@PostMapping("/storage/reduce")
public Result reduce(Long producetId,Integer nums)
sotrageService.reduce(p..Id,nums);
return Result.success("成功",null)
}
再新建MyBatisConfig,用于Mybatis和dao关联
@Configuration
@MappingScan({"com.springcloud.dao"})
public class MyBatisConfig{
}
对数据源进行代理(使用seata)
@Configuration
public class DataSourceProxyConfig{
@Value("${mybatis.mapperLacations}")
private String mapperLocation;
配置Druid数据源
@Bean
@ConfigurationPropertie(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource;
}
配置代理
@Bean
public DataSourceProxy proxy(DataSource dataSource){
return new DataSourceProxy(dataSource);
}
配置SqlSessionFactory...(不用记)
}
}
主启动类
需要取消数据源的自动配置,而是使用seata代理数据源
@SpringBootApplication(exclude = DataSourceAutoConfigration.class)
@EnableDiscoveryClient
@EnableFeignClient
SeatSource10010
开发accout_service(10012)模块
修改yml文件
port: 10012
name: seata_accout_service
datasource:
url:...
同样将file.conf和registry.conf引入
同样创建实体类
Accout{
id,userid,money
}
主启动类,AccountDao接口,AccountMapper.xml, Account_Service,Account_ServiceImple,Controller,MybatisConfig及代理全部类似
开发Order_service(10008)模块
application.yml
port: 10008
name: order_service
dataSource:
url: ...
经典file.conf和registry.conf到resources下
实体类
Order{
id...
}
Dao类
@Mapping
interface OrderDao{
#新建订单
void save(Order order);
#修改订单
void update(@Param("userId") Long userId...)
}
OderMapper.xml实现接口方法
Service接口
public interface OrderService{
void save(Order order);
}
OpenFeign接口调用Stroage
@FeignClient(value = "seata_storage_service") //此为nacos注册中心服务名
public interface StorageService{
@PostMapping("/storage/reduce") //远程调用的url http://seata_servic/storage/reduce
public Result reduce(Long productId,Integer nums)
}
同样再建个调用AccountService的
还有OrderService,这是调用自己就不是远程调用了
OrderServiceImple implements OrderService {
@Resource
private OrderService orderService;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
public void save(Order order){
orderDao.save(order); //调用本地方法生成订单order
accountService.reduce(order.getProductId,order.getNums()); //扣减余额
storageService.reduce(..) //扣除库存
orderDao.update(order.getUserId(),1) //调用本地方法修改订单状态
}
}
Order的Controller类
Order主启动类
PS:如果出现Service id not legal hostname是因为服务名称不能带下划线
@GlobalTransaction:完成分布式事务控制(出现异常也能保证数据一致性)
在Order的OrderServiceImpl加上可以控制全局事务
@GlobalTransaction(name = "save-order" //名称,自子指定
rollbackFor = Exception.class )
//指定发生什么异常就会回滚(这里是任何异常)
XID:全局唯一的事务ID
TC: 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
TM: 控制全局事务的边界,负责开启一个全局事务,并发起全局提交或全局回滚的决议
RM: 控制分值事务,负责分支注册,汇报状态,并接受事务协调器的指令,驱动分支(本地)事物的提交和回滚
RPC:远程调用
大致流程:
TM向TC申请开启一个全局事务并生成一个全局唯一XID,XID会在微服务调用链路上下文传播,RM向TC注册分支事务,将其纳入XID对应的全局事务管理,TM向TC发起针对XID的全局提交或回滚决议,TC调度XID管的全部分支事务完成提交或回滚请求
补:TM是被@GlobalTransactional标注的微服务(如上面的Order)
Mysql-Seata的global表会生成xid和事务组名称等;branch表管理各个微服务分支事务(RM);lock表给各个微服务事务加锁防止脏读
浏览器 微服务A 微服务B
X ====> TM:事务发起者 ===> 操作相关表
@GlobalTransactional
|| || || ||
||申请XID ||提交本地事务结果 || ||
||再RM || ||RM || 提交本地事务结果
\/ \/ || \/
||
\/
Mysql-Seata
global表,branch表,lock表
如果各个RM都执行成功就全局提交,否则全局回滚
Seata的事务模式
AT(默认,也是用的最多的),TCC,SAGA,XA
一阶段加载:Seata会拦截sql业务,在业务变更前保存为"before image",更新后的业务保存为"after image" 最后生成行锁,这样保证原子性
二阶段提交:Seata框架将一阶段保存的数据和行锁删掉,完成数据清理
二阶段回滚:若TC发现并不是所以RM都成功就会回滚到before还原业务数据