简单的看下例子:
采用spring boot ,需要redis,mysql,zookeeper(我们是用的这个做注册中心,可以换成其他的),详细见官网。
manager工程用于协调分布式服务
启动类加上注解@EnableTransactionManagerServer
@SpringBootApplication
@EnableTransactionManagerServer
public class LcnManagerApplication {
public static void main(String[] args) {
SpringApplication.run(LcnManagerApplication.class, args);
}
}
配置文件,这里需要配置redis,因为manager是通过redis来协调事物的
spring.application.name=tx-manager
server.port=7970
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
#redis 主机
spring.redis.host=127.0.0.1
#redis 端口
spring.redis.port=6379
pom.xml,需要加上tm
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-tm</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
service工程是互相调用
A->B->C
A工程@EnableDiscoveryClient开启注册与发现,我们用的是zookeeper,@EnableDistributedTransaction启动分布式事务
@SpringBootApplication
@EnableDiscoveryClient
@EnableDistributedTransaction
public class SpringServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(SpringServiceAApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
配置文件application.properties
这里用到了druid(完全可以不用),重要的是配置了manager地址,注意这里的端口号是8070(但是manager的端口是7970至于为什么是这样后面讲)
spring.application.name=txlcn-demo-spring-service-a
server.port=12011
## TODO 你的配置
spring.datasource.druid.async-init=true
spring.datasource.druid.async-close-connection-enable=true
# 配置获取连接等待超时时间
spring.datasource.druid.max-wait=50000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=30000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.druid.filters=stat,wall,log4j2
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/txlcn-demo?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
# 关闭Ribbon的重试机制(如果有必要)
ribbon.MaxAutoRetriesNextServer=0
ribbon.ReadTimeout=5000
ribbon.ConnectTimeout=5000
## tx-manager 配置
tx-lcn.client.manager-address=127.0.0.1:8070
tx-lcn.ribbon.loadbalancer.dtx.enabled=true
bootstrap.properties配置文件,我们用zookeeper用作配置与注册中心(端口号和ip换成自己的)
#zk连接地址
spring.cloud.zookeeper.connect-string=ip:port
#开启zk,默认为true 启动zk
spring.cloud.zookeeper.enabled=true
#开启zk config 启动zk外部化配置
spring.cloud.zookeeper.config.enabled=true
#开启zk discovery 启动zk服务注册与发现
spring.cloud.zookeeper.discovery.enabled=true
spring.cloud.zookeeper.discovery.register=true
#服务注册时使用ip地址而非主机名称
spring.cloud.zookeeper.discovery.preferIpAddress=true
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- servlet api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-tc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-txmsg-netty</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<!-- zk config和discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-all</artifactId>
<!-- 排除zk测试版本 -->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- zk -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
其他B与C工程和上述配置一样
然后看代码
A工程的逻辑代码,@Transactional本地事务,@LcnTransaction分布式事务(LCN模式,事务发起方)
@LcnTransaction
@Transactional
public String execute(String value, String exFlag) {
// step1. call remote ServiceD
String dResp = restTemplate.getForObject("http://127.0.0.1:12002/rpc?value=" + value, String.class);
// step2. call remote ServiceE
String eResp = serviceCClient.rpc(value);
// step3. execute local transaction
Demo demo = new Demo();
demo.setGroupId(TracingContext.tracing().groupId());
demo.setDemoField(value);
demo.setCreateTime(new Date());
demo.setAppName(Transactions.getApplicationId());
demoMapper.save(demo);
// 置异常标志,DTX 回滚
if (Objects.nonNull(exFlag)) {
throw new IllegalStateException("by exFlag");
}
return dResp + " > " + eResp + " > " + "ok-service-a";
}
B工程业务代码,@LcnTransaction(propagation = DTXPropagation.SUPPORTS)事务参与方
@LcnTransaction(propagation = DTXPropagation.SUPPORTS)
@Transactional
public String rpc(String value) {
Demo demo = new Demo();
demo.setGroupId(TracingContext.tracing().groupId());
demo.setDemoField(value);
demo.setAppName(Transactions.getApplicationId());
demo.setCreateTime(new Date());
demoMapper.save(demo);
return "ok-service-b";
}
C工程业务代码,@LcnTransaction(propagation = DTXPropagation.SUPPORTS)事务参与方
@LcnTransaction(propagation = DTXPropagation.SUPPORTS)
@Transactional
public String rpc(String value) {
Demo demo = new Demo();
demo.setDemoField(value);
demo.setCreateTime(new Date());
demo.setAppName(Transactions.getApplicationId());
demo.setGroupId(TracingContext.tracing().groupId());
demoMapper.save(demo);
return "ok-service-c";
}
这样无论在哪一部分出错,都不会插入数据库成功