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

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

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

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

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

完整的代码示例已经上传至Github, 喜欢的话可以给我一颗star, 这样能激励我写出更好的文章

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的使用或者对本人的编码风格有更好的想法或者建议, 希望能在下方给我留言, 再次感谢你的耐心阅读

作者:Chris
原博客:http://blog.chriscs.com
Github:https://github.com/prontera

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容