Fescar是阿里巴巴开源的分布式事务中间件,以高效并且对业务0侵入的方式,解决微服务场景下面临的分布式事务问题。
官方介绍:https://github.com/seata/seata/wiki/概览
(一)软件版本
环境 | 版本 | 备注 |
---|---|---|
操作系统 in PC | Window 10 企业版 | 安装JDK、VMWare、Maven、Intellij IDEA |
操作系统 in VMWare | CentOS Linux release 7.6.1810 | 安装Docker |
VMWare | 15.0.0 build-10134415 | |
Intellij IDEA | IntelliJ IDEA 2018.3.2 (Ultimate Edition) | |
JDK in PC | 1.8.0_171 | |
JDK in VMWare | 1.8.0_201 | |
Maven | 3.5.3 | |
MySQL | 8.0.15 | VMware中Docker镜像安装 |
Docker in VMWare | 18.09.4 |
(二)第三方库版本
名称 | 版本 | 备注 |
---|---|---|
SpringBoot | 1.5.15.RELEASE | |
SpringCloud | Edgware.SR4 | |
Lombok | 1.16.20 | |
Fescar | 0.4.1 | |
Druid | 1.1.15 | |
Mybatis-spring | 1.2.2 |
(一)MySQL
1.创建2个数据库:test_a和test_b
2.在test_a数据库中:
(1)创建表t_test_a:
-- ----------------------------
-- Table structure for t_test_a
-- ----------------------------
DROP TABLE IF EXISTS `t_test_a`;
CREATE TABLE `t_test_a` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
(2)创建表undo_log:
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime(0) NULL,
`log_modified` datetime(0) NULL,
`ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_unionkey`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 192 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
3.在test_b数据库中:
(1)创建表t_test_b:
-- ----------------------------
-- Table structure for t_test_b
-- ----------------------------
DROP TABLE IF EXISTS `t_test_b`;
CREATE TABLE `t_test_b` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
(2)创建表undo_log:
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime(0) NULL,
`log_modified` datetime(0) NULL,
`ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_unionkey`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 192 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
(二)fescar-server
1、下载Fescar-server,解压,运行
2、下载地址:https://github.com/seata/seata/releases
3、解压:tar -zxvf fescar-server-0.4.1.tar.gz -C fescar-server-0.4.1
4、运行:sh fescar-server-0.4.1/bin/fescar-server.sh 8091 fescar-server-0.4.1/data/ 192.168.201.8
5、运行指令最后指定server的访问IP,解决多IP环境部署问题
(一)完整代码
(二)创建Demo工程
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-config
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.cloud
spring-cloud-starter-feign
org.projectlombok
lombok
${lombok.version}
org.springframework.boot
spring-boot-starter-test
test
(三)创建Eureka注册中心服务
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
server:
port: 8301
spring:
application:
name: eureka-server
cloud:
config:
enabled: false
eureka:
instance:
hostname: localhost
prefer-ip-address: true
server:
enable-self-preservation: true
eviction-interval-timer-in-ms: 5000
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://${eureka.instance.hostname}:8301/eureka/
security:
basic:
enabled: false
org.springframework.cloud
spring-cloud-starter-eureka-server
org.springframework.cloud
spring-cloud-starter-security
(四)创建fescar-config配置模块
/**
* fescar相关配置
*
* @author [email protected]
* @date 2019-4-8 13:43:19
*/
@Configuration
public class FescarAutoConfiguration {
public static final String FESCAR_XID = "Fescar_XID";
/**
* 使用fescar代理数据源
*/
@Bean
public DataSource dataSource(Environment environment) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
try {
dataSource.setDriver(DriverManager.getDriver(environment.getProperty("spring.datasource.url")));
} catch (SQLException e) {
throw new RuntimeException("can not recognize datasource driver");
}
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
return new DataSourceProxy(dataSource);
}
/**
* 全局事务扫描,设置appName和groupName
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(Environment environment) {
String applicationName = environment.getProperty("spring.application.name");
String groupName = environment.getProperty("fescar.group.name");
if (applicationName == null) {
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName);
} else {
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName);
}
}
/**
* 为请求添加拦截器
*/
@Bean
public Object addFescarInterceptor(Collection restTemplates) {
restTemplates.stream().forEach(restTemplate -> {
List interceptors = restTemplate.getInterceptors();
if (interceptors != null) {
interceptors.add(fescarRestInterceptor());
}
});
return new Object();
}
@Bean
public FescarRMRequestFilter fescarRMRequestFilter() {
return new FescarRMRequestFilter();
}
@Bean
public FescarRestInterceptor fescarRestInterceptor() {
return new FescarRestInterceptor();
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
/**
* 请求过滤器,获取XID并绑定到上下文中
*
* @author [email protected]
* @date 2019-4-8 13:31:50
*/
@Slf4j
public class FescarRMRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String currentXID = request.getHeader(FescarAutoConfiguration.FESCAR_XID);
if (!StringUtils.isEmpty(currentXID)) {
RootContext.bind(currentXID);
log.info("current request bind XID:{}", currentXID);
}
try {
filterChain.doFilter(request, response);
} finally {
String unbindXID = RootContext.unbind();
if (unbindXID != null) {
log.info("current request unbind XID:{}", unbindXID);
if (!currentXID.equals(unbindXID)) {
log.info("XID is changed when request execute, check if it meets expectations please");
}
}
if (currentXID != null) {
log.info("XID is changed when request execute, check if it meets expectations please");
}
}
}
}
/**
* 将上下文中的XID放到请求头中
*
* @author [email protected]
* @date 2019-4-8 13:38:08
*/
public class FescarRestInterceptor implements RequestInterceptor, ClientHttpRequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String xid = RootContext.getXID();
if (!StringUtils.isEmpty(xid)) {
requestTemplate.header(FescarAutoConfiguration.FESCAR_XID, xid);
}
}
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
String xid = RootContext.getXID();
if (!StringUtils.isEmpty(xid)) {
HttpHeaders httpHeaders = httpRequest.getHeaders();
httpHeaders.put(FescarAutoConfiguration.FESCAR_XID, Collections.singletonList(xid));
}
return clientHttpRequestExecution.execute(httpRequest, bytes);
}
}
org.springframework.cloud
spring-cloud-starter
com.alibaba
druid
${druid.version}
com.alibaba.fescar
fescar-tm
${fescar.version}
com.alibaba.fescar
fescar-spring
${fescar.version}
(五)创建service-a服务
操作test_a数据库,同时通过feign调用service-b,从而操作test_b数据库
ServiceBClient.java
/**
* 调用service-b的客户端
*
* @author [email protected]
* @date 2019-4-8 19:00:00
*/
@FeignClient(name = "server-b")
@RequestMapping("/b")
public interface ServiceBClient {
/**
* 根据id查询记录
*
* @param id id
* @return 记录
*/
@GetMapping("/{id}")
String get(@PathVariable(value = "id") Long id);
/**
* 查询所有记录
*
* @return 所有记录
*/
@GetMapping("/list")
String list();
/**
* 添加一个记录
*
* @param dto 参数
* @return 是否成功
*/
@PostMapping("/add")
String add(@RequestBody AddDTO dto);
}
@Service
public class TestService {
@Autowired
private TestAMapper testMapper;
@Autowired
private ServiceBClient serviceBClient;
/**
* 添加记录到A和B
*
* @param dto 参数
*/
@Transactional(rollbackFor = Exception.class)
public void addToABS(AddDTO dto) {
testMapper.insert(dto);
serviceBClient.add(dto);
}
/**
* 添加记录到A和B,最后回退
*
* @param dto 参数
*/
@GlobalTransactional(name = "fescar-test-tx")
public void addToABF(AddDTO dto) {
testMapper.insert(dto);
serviceBClient.add(dto);
throw new RuntimeException("mock exception");
}
}
spring:
datasource:
url: jdbc:mysql://xxx
username: xxx
password: xxx
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
logging:
level:
ROOT: info
org.mybatis: debug
mybatis:
mapper-locations: classpath:mapper/*.xml
fescar:
group:
name: test_group #fescar组名
bootstrap.yml
server:
port: 8302
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
hostname: ${spring.cloud.client.ipAddress}
lease-renewal-interval-in-seconds: 3
lease-expiration-duration-in-seconds: 7
client:
service-url:
defaultZone: ${EUREKA_DEFAULT_ZONE:http://localhost:8301/eureka/}
spring:
application:
name: server-a
cloud:
config:
enabled: false
org.springframework.cloud
spring-cloud-starter-zipkin
com.alibaba
druid
${druid.version}
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis-spring.version}
mysql
mysql-connector-java
com.hand
fescar-config
1.0-SNAPSHOT
registry {
# file nacos
type = "file"
file {
name = "file.conf"
}
}
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
}
store {
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
}
service {
#vgroup->rgroup
vgroup_mapping.xxxx = "default" #xxxx改为fescar组名
#only support single node
default.grouplist = "xxxx" #xxxx改为fescar-server的IP和端口
#degrade current not support
enableDegrade = false
#disable
disable = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
}
(六)创建service-b服务
操作test_b数据库,提供接口给service-a调用
实现了简单的记录插入操作,提供API,详细内容请查看完整代码,这里不再赘述。
(一)启动注册中心、server-a和server-b
(二)调用接口:localhost:8302/a/addToABS
(三)调用接口:localhost:8302/a/addToABF