Spring Cloud综合实战 - 基于TCC补偿模式的分布式事务

Spring Cloud Netflix的Try-Confirm-Cancel柔性事务示例

Posted by Chris on April 30, 2017

本文通过使用Spring Cloud和Docker构建了一个常见的Microservice体系.

Spring Cloud为开发者提供了快速构建分布式系统中的一些常见工具, 如分布式配置中心, 服务发现与注册中心, 智能路由, 服务熔断及降级, 消息总线等.

而Spring Cloud Sleuth为Spring Cloud提供了分布式追踪方案, 可视化地分析服务调用链路和服务间的依赖关系

本次实战以模拟下单流程作为实战演示, 使用Try-Confirm-Cancel即TCC模式为分布式事务提供最终一致性.

完整的代码示例已经上传至Github

Try Confirm Cancel补偿模式

本实例遵循的是Atomikos公司对微服务的分布式事务所提出的RESTful TCC解决方案

RESTful TCC模式分3个阶段执行

  1. Trying阶段主要针对业务系统检测及作出预留资源请求, 若预留资源成功, 则返回确认资源的链接与过期时间
  2. Confirm阶段主要是对业务系统的预留资源作出确认, 要求TCC服务的提供方要对确认预留资源的接口实现幂等性, 若Confirm成功则返回204, 资源超时则证明已经被回收且返回404
  3. Cancel阶段主要是在业务执行错误或者预留资源超时后执行的资源释放操作, Cancel接口是一个可选操作, 因为要求TCC服务的提供方实现自动回收的功能, 所以即便是不认为进行Cancel, 系统也会自动回收资源

对RESTful TCC事务更为详细的解释可以点击这里进行阅读

系统结构

基础组件

Zuul Gateway

Zuul在本实例中仅作为路由所使用, 配置降低Ribbon的读取与连接超时上限

Eureka H.A.

多个对等Eureka节点组成高可用集群, 并将注册列表的自我保护的阈值适当降低

Config Server

  • 如果远程配置中有密文{cipher}*, 那么该密文的解密将会延迟至客户端启动的时候. 因此客户端需要配置AES的对称密钥encrypt.key, 并且客户端所使用的JRE需要安装Java 8 JCE, 否则将会抛出Illegal key size相关的异常. (本例中Docker Compose构建的容器已经安装了JCE, 如果远程配置文件没有使用{cipher}*也不必进行JCE的安装)

    spring:
      cloud:
        config:
          server:
            git:
              uri: 'https://git.oschina.net/witless/conf-repo.git'
              clone-on-start: true
            encrypt:
              enabled: false
      application:
        name: 'config-server'
    
  • 为了达到开箱即用, 选用公开仓库Github或者GitOsc

  • 本项目中有两个自定义注解 @com.github.prontera.Delay 控制方法的延时返回时间

    @com.github.prontera.RandomlyThrowsException 随机抛出异常, 人为地制造异常

    默认的远程配置如下

    solar:
      delay:
        time-in-millseconds: 0
      exception:
        enabled: false
        factor: 7
    

    这些自定义配置正是控制方法返回的时延, 随机异常的因子等

    我在服务order, product, accounttcc中的所有Controller上都添加了以上两个注解, 当远程配置的更新时候, 可以手工刷新/refresh或通过webhook等方法自动刷新本地配置. 以达到模拟微服务繁忙或熔断等情况.

RabbitMQ

原本作为可靠性事件投递的Broker, 如今被TCC模式所替代. 可为日后的Spring Cloud Steam或Spring Cloud Bus的集成作为基础组件而保留

监控服务

Spring Boot Admin

此应用提供了管理Spring Boot服务的简单UI, 下图是在容器中运行时的服务健康检测页

Hystrix Dashboard

提供近实时依赖的统计和监控面板, 以监测服务的超时, 熔断, 拒绝, 降级等行为

Zipkin Server

Zipkin是一款开源的分布式实时数据追踪系统, 其主要功能是聚集来自各个异构系统的实时监控数据, 用来追踪微服务架构下的系统时延问题. 下图是对order服务的请求进行追踪的情况

业务服务

首次启动时通过Flyway自动初始化数据库

对spring cloud config server采用fail fast策略, 一旦远程配置服务无法连接则无法启动业务服务

account

用于获取用户信息, 用户注册, 修改用户余额, 预留余额资源, 确认预留余额, 撤销预留余额

product

用于获取产品信息, 变更商品库存, 预留库存资源, 确认预留库存, 撤销预留库存

tcc coordinator

TCC资源协调器, 其职责如下

  • 对所有参与者发起Confirm请求
  • 无论是协调器发生的错误还是调用参与者所产生的错误, 协调器都必须有自动恢复重试功能, 尤其是在确认的阶段, 以防止网络抖动的情况

order

order服务是本项目的入口, 尽管所提供的功能很简单

  • 下单. 即生成预订单, 为了更好地测试TCC功能, 在下单时就通过Feign向服务accountproduct发起预留资源请求, 并且记录入库
  • 确认订单. 确认订单时根据订单ID从库中获取订单, 并获取预留资源确认的URI, 交由服务tcc统一进行确认, 如果发生冲突即记录入库, 等待人工处理

与其他服务进行通讯, 我们选择使用Feign

/**
 * @author Zhao Junjian
 */
@FeignClient(name = TccClient.SERVICE_ID, fallback = TccClientFallback.class)
public interface TccClient {
    /**
     * eureka service name
     */
    String SERVICE_ID = "tcc";
    /**
     * api prefix
     */
    String API_PATH = "/api/v1/coordinator";

    @RequestMapping(value = API_PATH + "/confirmation", method = RequestMethod.PUT, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    void confirm(@RequestBody TccRequest request);

    @RequestMapping(value = API_PATH + "/cancellation", method = RequestMethod.PUT, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    void cancel(@RequestBody TccRequest request);

}

Swagger UI

Swagger的目标是为REST APIs 定义一个标准的, 与语言无关的接口, 使人和计算机在看不到源码或者看不到文档或者不能通过网络流量检测的情况下能发现和理解各种服务的功能. 当服务通过Swagger定义, 消费者就能与远程的服务互动通过少量的实现逻辑. 类似于低级编程接口, Swagger去掉了调用服务时的很多猜测.

运行

Docker Compose运行

在项目根路径下执行脚本build.sh, 该脚本会执行Maven的打包操作, 并会迭代目录下的*-compose.yml进行容器构建

构建完成后需要按照指定的顺序启动

  1. 启动MySQL, RabbitMQ等基础组件

    ➜  solar git:(feature/cleanup) ✗ docker-compose -f infrastructure-compose.yml up -d
    
  2. 启动Eureka Server与Config Server

    ➜  solar git:(feature/cleanup) ✗ docker-compose -f basic-ms-compose.yml up -d
    
  3. 启动监控服务

    ➜  solar git:(feature/cleanup) ✗ docker-compose -f monitor-ms-compose.yml up -d
    
  4. 启动业务服务

    ➜  solar git:(feature/cleanup) ✗ docker-compose -f business-ms-compose.yml up -d
    

IDE运行

因为程序本身按照Docker启动, 所以对于hostname需要在hosts文件中设置正确才能正常运行

## solar
127.0.0.1 eureka1
127.0.0.1 eureka2
127.0.0.1 rabbitmq
127.0.0.1 zipkin_server
127.0.0.1 solar_mysql
127.0.0.1 gitlab

根据依赖关系, 程序最好按照以下的顺序执行

docker mysql > docker rabbitmq > eureka server > config server > zipkin server > 其他微服务

示例

根据附表中的服务字典, 我们通过Zuul或Swagge对order服务进行预订单生成操作

POST http://localhost:7291/order/api/v1/orders
Content-Type: application/json;charset=UTF-8

{
  "product_id": 7,
  "user_id": 1
}

成功后我们将得到预订单的结果

{
  "data": {
    "id": 15,
    "create_time": "2017-03-28T18:18:02.206+08:00",
    "update_time": "1970-01-01T00:00:00+08:00",
    "delete_time": "1970-01-01T00:00:00+08:00",
    "user_id": 1,
    "product_id": 7,
    "price": 14,
    "status": "PROCESSING"
  },
  "code": 20000
}

此时我们再确认订单

(如果想测试预留资源的补偿情况, 那么就等15s后过期再发请求, 注意容器与宿主机的时间)

POST http://localhost:7291/order/api/v1/orders/confirmation
Content-Type: application/json;charset=UTF-8

{
  "order_id": 15
}

如果成功确认则返回如下结果

{
  "data": {
    "id": 15,
    "create_time": "2017-03-28T18:18:02.206+08:00",
    "update_time": "2017-03-28T18:21:32.78+08:00",
    "delete_time": "1970-01-01T00:00:00+08:00",
    "user_id": 1,
    "product_id": 7,
    "price": 14,
    "status": "DONE"
  },
  "code": 20000
}

至此就完成了一次TCC事务, 当然你也可以测试超时和冲突的情况, 这里就不再赘述

拓展

使用Gitlab作为远程配置仓库

本例中默认使用Github或GitOsc中的公开仓库, 出于自定义的需要, 我们可以在本地构建Git仓库, 这里选用Gitlab为例.

将以下配置添加至docker compose中的文件中并启动Docker Gitlab容器

gitlab:
    image: daocloud.io/daocloud/gitlab:8.16.7-ce.0
    ports:
        - "10222:22"
        - "80:80"
        - "10443:443"
    volumes:
        - "./docker-gitlab/config/:/etc/gitlab/"
        - "./docker-gitlab/logs/:/var/log/gitlab/"
        - "./docker-gitlab/data/:/var/opt/gitlab/"
    environment:
        - TZ=Asia/Shanghai

将项目的config-repo添加至Gitlab中, 并修改config-ms中git仓库的相关验证等参数即可

结语

更为详细的说明及代码示例已经上传至Github

https://github.com/prontera/spring-cloud-rest-tcc

如有对本项目中的Spring Cloud的使用或者对本人的编码风格有更好的想法或者建议, 希望能在下方给我留言, 再次感谢你的耐心阅读