分布式事务框架Fescar在SpringCloud环境下的应用实践

一、Fescar简介

Fescar是阿里巴巴开源的分布式事务中间件,以高效并且对业务0侵入的方式,解决微服务场景下面临的分布式事务问题。
官方介绍:https://github.com/seata/seata/wiki/概览

二、设计思路

  • 通过API调用服务A,服务A中先对数据库A进行插入操作,然后通过feign调用服务B,在服务B中对数据库B进行插入操作,调用服务B完成后,此时数据库A和数据库B中都有新插入的记录,接着服务A中抛出异常,此时数据库A与数据库B中的数据都应该回滚,即最终都没有新插入的记录

三、开发环境

(一)软件版本

环境 版本 备注
操作系统 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环境部署问题

五、实践步骤

(一)完整代码

  • 完整工程代码:https://github.com/zhangzhentao1995/fescar-demo

(二)创建Demo工程

  • 创建父工程,配置公共依赖
  • 整体工程目录:
    分布式事务框架Fescar在SpringCloud环境下的应用实践_第1张图片
  • pom.xml


    
    
        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注册中心服务

  • 在父工程中创建注册中心模块
  • EurekaApplication.java
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}
  • bootstrap.xml
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
  • pom.xml
    
        
            org.springframework.cloud
            spring-cloud-starter-eureka-server
        
        
            org.springframework.cloud
            spring-cloud-starter-security
        
    

(四)创建fescar-config配置模块

  • 创建fescar公共配置,提供给需要使用fescar的服务引用
  • FescarAutoConfiguration.java
/**
 * 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();
    }
}
  • FescarRMRequestFilter.java
/**
 * 请求过滤器,获取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");
            }
        }
    }
}
  • FescarRestInterceptor.java
/**
 * 将上下文中的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);
    }
}
  • pom.xml
   
        
            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);
}
  • TestService.java
@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");
    }
}
  • application.yml
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
  • pom.xml
   
        
            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.conf:指定fescar配置方式和配置文件
registry {
  # file nacos
  type = "file"
  file {
    name = "file.conf"
  }
}
  • file.conf:fescar配置文件,
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
分布式事务框架Fescar在SpringCloud环境下的应用实践_第2张图片

  • 查看数据库test_a中的表t_test_a和数据库test_b中的表t_test_b,发现均新增一个新的记录。

(三)调用接口:localhost:8302/a/addToABF
分布式事务框架Fescar在SpringCloud环境下的应用实践_第3张图片

  • 查看表t_test_a和表t_test_b均无新记录插入,说明回滚成功。

七、完整工程代码

  • https://github.com/zhangzhentao1995/fescar-demo

你可能感兴趣的:(微服务)