依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
数据库: 8.0.25 MySQL Community Server - GPL
创建mysql实例与表(docker)
docker run --name db1 -e MYSQL_ROOT_PASSWORD=111111 -v ~/soft/mysql/1/data:/var/lib/mysql -p 3308:3306 -itd mysql
docker run --name db2 -e MYSQL_ROOT_PASSWORD=111111 -v ~/soft/mysql/2/data:/var/lib/mysql -p 3309:3306 -itd mysql
db1 创建表
create database test;
create table account( id int, account int);
insert account values (1,100);
db2 创建表
create database test;
create table account( id int, account int);
insert account values (2,100);
springboot seata+nacos 配置
该配置仅适用1.4.0,1.4.2会发生数据不会滚的情况,尚未发现原因。。。
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
seata:
tx-service-group: my_test_tx_group # 值与seata源码中 config.txt中的service.vgroupMapping的后缀相同,否则服务启动会提示找不到可用的service
registry:
type: nacos
nacos:
server-addr: ${discovery.server-addr}
username: ${discovery.username}
password: ${discovery.password}
group: ${discovery.group}
config:
type: nacos
nacos:
server-addr: ${discovery.server-addr}
username: ${discovery.username}
password: ${discovery.password}
group: ${discovery.group}
service:
vgroup-mapping:
my_test_tx_group: default
提交事务
单个服务
controller
@GlobalTransactional
@GetMapping
public Object test(){
globalServicel.testGlobal();
return 0;
}
service
public class GlobalService {
@Autowired
AccountMapper accountMapper;
public void testGlobal(){
accountMapper.update();
// HttpUtils.get("http://localhost:9101/test", null);
throw new RuntimeException("12");
}
}
当提交事务时日志中可以看到开启了一个全局事务与一个子事务,若是只有global事务没有Branch提示可能是哪里出了错(1.4.2出了这个问题)
还有一点要查看sql是否更新了数据,比如update xxx from xxx where id=1这条数据不存在,因此没有更新任何数据时候,seata日志将不会显示commit,此时要注意查看,有可能不是没有开启事务,只是没有发生数据的更改而已
事务回滚时候
调用第二个服务
service
public class GlobalService {
@Autowired
AccountMapper accountMapper;
public void testGlobal(){
accountMapper.update();
HttpUtils.get("http://localhost:9101/test", null);
}
}
需要注意的是这里进行http通信时候要把xid传过去,feign被seata改写了默认会传过去,自己构造的http要手动传一下
public static String get(String url, Map<String, String> headers) {
HttpGet req = new HttpGet(url);
String xId = RootContext.getXID();
log.info("xid:{}", xId);
if (headers == null){
headers = new HashMap<>();
}
headers.put("TX_XID", xId);
addHeader(req, headers);
return execute(req);
}
被调用的服务要接收这个xid绑定到RootContext里面,starter中有个SeataHandlerInterceptor已经被声明了,会自己拦截header绑定xid,有特殊需求我们可以自己写一下,手动绑定
public class SeataHandlerInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(SeataHandlerInterceptor.class);
public SeataHandlerInterceptor() {
log.info("拦截器初始化成功");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String xid = RootContext.getXID();
String rpcXid = request.getHeader("TX_XID");
if (log.isDebugEnabled()) {
log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
}
if (StringUtils.isBlank(xid) && rpcXid != null) {
RootContext.bind(rpcXid);
if (log.isDebugEnabled()) {
log.debug("bind {} to RootContext", rpcXid);
}
}
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
if (StringUtils.isNotBlank(RootContext.getXID())) {
String rpcXid = request.getHeader("TX_XID");
if (StringUtils.isEmpty(rpcXid)) {
return;
}
String unbindXid = RootContext.unbind();
if (log.isDebugEnabled()) {
log.debug("unbind {} from RootContext", unbindXid);
}
if (!rpcXid.equalsIgnoreCase(unbindXid)) {
log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid);
if (unbindXid != null) {
RootContext.bind(unbindXid);
log.warn("bind {} back to RootContext", unbindXid);
}
}
}
}
}
然后第2个服务就可以开始子事务了,需要注意这里不需要加事务直接,有了xid seata会帮我们自动开启事务
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private Db1Service db1Service;
@GetMapping
public Object test(){
db1Service.test1();
return 0;
}
}
@Service
public class Db1Service {
@Autowired
private AccountMapper accountMapper;
public void test1(){
accountMapper.update();
throw new RuntimeException("ee");
}
}