Go 微服务:基于 RabbitMQ 和 AMQP 进行消息传递

欢迎你们前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~html

本文来自 云+社区翻译社,由 Tnecesoc编译。

介绍

微服务就是将应用程序的业务领域划分为具备明确范围的不一样场景,并以分离的流程来运行这些场景,使得其中跨边界的任何持久化的关系必须依赖最终的一致性,而不是 ACID 类事务或外键约束。这些概念不少都来源于领域驱动设计(DDD),或受到了它的启发。不过 DDD 是个要花一整个博客系列来说的话题,这里就先不提了。git

在咱们的 Go 微服务系列博客还有微服务架构的背景下,实现服务间松耦合的一种方式是引入消息传递机制来进行不须要遵循严格的请求 / 响应式的消息交换或相似机制的服务的通讯。这里要强调一点,引入消息传递机制只是众多能够用来实现服务间松耦合的策略之一。github

正如咱们在博客系列的第 8 章看到的那样,在 Spring Cloud 里,Spring Cloud Config 服务器将 RabbitMQ 做为了运行时的一个依赖项目,所以 RabbitMQ 应该是一个不错的消息中继器(message broker)。spring

至于本系列博客的这一章的内容,咱们会在读取特定的账号对象的时候让咱们的 "account service" 往 RabbitMQ 的交换器里面放一条消息。这一消息会被一个咱们会在这篇博客里编写的,全新的微服务所处理。咱们同时也会将一些 Go 语言代码放在一个 “common” 库里面来实如今跨微服务情景下的代码复用。docker

记住第 1 部分的系统图景吗?下面是完成这一章的部分以后的样子:shell

img

在完成这一部分以前咱们要作不少工做,不过咱们可以作到。数据库

源代码

这部分会有不少新代码,咱们不可能把它全放在博客文章里。若要取得完整的源代码,不妨用 git clone 命令下载下来,并切换到第 9 章的分支:json

git checkout P9

发送消息

咱们将实施一个简单的仿真用例:当在 “account service” 中读取某些 “VIP” 帐户时,咱们但愿触发 “VIP offer” 服务。这一服务在特定状况下会为帐户持有者生成一个 “offer” 。在一个设计正确的领域模型里面,accounts 对象还有 VIP offer 对象都是两个独立的领域,而它们应尽量少地了解彼此。数组

img

这个 account service 应当从不直接访问 VIP service 的存储空间(即 offers)。在这种状况下,咱们应该把一条消息传递给 RabbitMQ 上的 “vip service”,并将业务逻辑和持久化存储彻底委任给 “vip service”。服务器

咱们将使用 AMQP 协议来进行全部通讯,AMQP 协议是一个做为 ISO 标准的应用层协议,其所实现的消息传递能为系统带来可互操做性。这里咱们就延用在第 8 章咱们处理配置更新(configuration update)时候的设置,选用 streadway / amqp 这一 AMQP 库。

让咱们重述一下 AMQP 中的交换器(exchange)与发布者(publisher)、消费者(consumer)和队列(queue)的关系:

img

发布者会将一条消息发布到交换点,后者会根据必定的路由规则或登记了相应消费者的绑定信息来说消息的副本发到队列(或分发到多个队列)里去。对此,Quora 上的这个回答有个很好的解释。

与消息传递有关的代码

因为咱们想要使用新的以及现有的代码来从咱们现有的 account service 和新的 vip service 里面的 Spring Cloud 配置文件里面加载咱们所需的配置,因此咱们会在这里建立咱们的第一个共享库。

首先在 /goblog 下建立新文件夹 common 来存放可重用的内容:

mkdir -p common/messaging
mkdir -p common/config

咱们将全部与 AMQP 相关的代码放在 messaging 文件夹里面,并将配置文件放在 config 文件夹里。你也能够将 /goblog/accountservice/config 的内容复制到 /goblog/common/config 中 - 请记住,这要求咱们更新先前从 account service 中导入配置代码的 import 语句。不妨看看完整源代码来查阅这部分的写法。

跟消息传递有关的代码会被封装在一个文件中,该文件将定义应用程序用来链接,发布和订阅消息的接口还有实际实现。实际上,咱们用的 streadway / amqp 已经提供了不少实现 AMQP 消息传递所需的模板代码,因此这部分的具体细节也便不深究了。

在 /goblog/common/messaging 中建立一个新的 .go 文件:messagingclient.go

让咱们看看里面主要应有什么:

// 定义用来链接、发布消息、消费消息的接口
type IMessagingClient interface {
        ConnectToBroker(connectionString string)
        Publish(msg []byte, exchangeName string, exchangeType string) error
        PublishOnQueue(msg []byte, queueName string) error
        Subscribe(exchangeName string, exchangeType string, consumerName string, handlerFunc func(amqp.Delivery)) error
        SubscribeToQueue(queueName string, consumerName string, handlerFunc func(amqp.Delivery)) error
        Close()
}

上面这段代码定义了咱们所用的消息接口。这就是咱们的 “account service” 和 “vip service” 在消息传递时将要处理的问题,能经过抽象手段来消除系统的大部分复杂度。请注意,我选择了两个 “Produce” 和 “Consume” 的变体,以便与订阅/发布主题还有 direct / queue 消息传递模式合在一块儿使用。

接下来,咱们将定义一个结构体,该结构体将持有指向 amqp.Connection 的指针,而且咱们将再加上一些必要的方法,以便它能(隐式地,Go 一直以来都是这样)实现咱们刚才声明的接口。

// 接口实现,封装了一个指向 amqp.Connection 的指针
type MessagingClient struct {
        conn *amqp.Connection
}

接口的实现很是冗长,在此只给出其中两个 - ConnectToBroker()PublishToQueue()

func (m *MessagingClient) ConnectToBroker(connectionString string) {
        if connectionString == "" {
                panic("Cannot initialize connection to broker, connectionString not set. Have you initialized?")
        }
        var err error
        m.conn, err = amqp.Dial(fmt.Sprintf("%s/", connectionString))
        if err != nil {
                panic("Failed to connect to AMQP compatible broker at: " + connectionString)
        }
}

这就是咱们得到 connection 指针 (如 amqp.Dial) 的方法。若是咱们丢掉了配置文件,或者链接不上中继器,那么微服务就会抛出一个 panic 异常,并会让容器协调器从新建立一个新的实例。在这里传入的 connectionString 参数就以下例所示:

amqp://guest:guest@rabbitmq:5672/

注意,这里的 rabbitmq broker 是以 service 这一 Docker Swarm 的模式下运行的。

PublishOnQueue() 函数很长 - 它跟官方提供的 streadway 样例或多或少地有些不一样,毕竟这里简化了它的一些参数。为了将消息发布到一个有名字的队列,咱们须要传递这些参数:

  • body - 以字节数组的形式存在。能够是 JSON,XML 或一些二进制文件。
  • queueName - 要发送消息的队列的名称。

若要了解交换器的更多详情,那请参阅 RabbitMQ 文档

func (m *MessagingClient) PublishOnQueue(body []byte, queueName string) error {
        if m.conn == nil {
                panic("Tried to send message before connection was initialized. Don't do that.")
        }
        ch, err := m.conn.Channel()      // 从 connection 里得到一个 channel 对象
        defer ch.Close()
        // 提供一些参数声明一个队列,若相应的队列不存在,那就建立一个
        queue, err := ch.QueueDeclare(
                queueName, // 队列名
                false, // 是否持久存在
                false, // 是否在不用时就删掉
                false, // 是否排外
                false, // 是否无等待
                nil, // 其余参数
        )
        // 往队列发布消息
        err = ch.Publish(
                "", // 目标为默认的交换器
                queue.Name, // 路由关键字,例如队列名
                false, // 必须发布
                false, // 当即发布
                amqp.Publishing{
                        ContentType: "application/json",
                        Body:        body, // JSON 正文, 以 byte[] 形式给出
                })
        fmt.Printf("A message was sent to queue %v: %v", queueName, body)
        return err
}

这里的模板代码略多,但应该不难理解。这段代码会声明一个(若是不存在那就建立一个)队列,而后把咱们的消息以字节数组的形式发布给它。

将消息发布到一个有名字的交换器的代码会更复杂,由于它须要一段模板代码来声明交换器,以及队列,并把它们绑定在一块儿。这里有一份完整的源代码示例。

接下来,因为咱们的 “MessageClient” 的实际使用者会是 /goblog/accountservice/service/handlers.go ,咱们会往里面再添加一个字段,并在请求的账户有 ID “10000” 的时候往硬编码进程序中的 “is VIP” 检查方法中发送一条消息:

var DBClient dbclient.IBoltClient
var MessagingClient messaging.IMessagingClient     // NEW
func GetAccount(w http.ResponseWriter, r *http.Request) {
     ...

而后:

...
    notifyVIP(account)   // 并行地发送 VIP 消息
    // 如有这样的 account, 那就把它弄成一个 JSON, 而后附上首部和其余内容来打包
    data, _ := json.Marshal(account)
    writeJsonResponse(w, http.StatusOK, data)
}
// 若是这个 account 是咱们硬编码进来的 account, 那就开个协程来发送消息
func notifyVIP(account model.Account) {
        if account.Id == "10000" {
                go func(account model.Account) {
                        vipNotification := model.VipNotification{AccountId: account.Id, ReadAt: time.Now().UTC().String()}
                        data, _ := json.Marshal(vipNotification)
                        err := MessagingClient.PublishOnQueue(data, "vipQueue")
                        if err != nil {
                                fmt.Println(err.Error())
                        }
                }(account)
        }
}

正好借此机会展现一下调用一个新的协程(goroutine)时所使用的内联匿名函数,即便用 go 关键字。咱们不能由于要执行 HTTP 处理程序发送消息就把 “主” 协程阻塞起来,所以这也是增长一点并行性的好时机。

main.go 也须要有所更新,以便在启动的时候能使用加载并注入到 Viper 里面的配置信息来初始化 AMQ 链接。

// 在 main 方法里面调用这个函数
func initializeMessaging() {
if !viper.IsSet("amqp_server_url") {
panic("No 'amqp_server_url' set in configuration, cannot start")
}
service.MessagingClient = &messaging.MessagingClient{}
service.MessagingClient.ConnectToBroker(viper.GetString("amqp_server_url"))
service.MessagingClient.Subscribe(viper.GetString("config_event_bus"), "topic", appName, config.HandleRefreshEvent)
}

这段没什么意思 - 咱们经过建立一个空的消息传递结构,并使用从 Viper 获取的属性值来调用 ConnectToBroker 来获得 service.MessagingClient 实例。若是咱们的配置没有 broker_url,那就抛一个 panic 异常,毕竟在不可能链接到中继器的时候程序也没办法运行。

更新配置

咱们在第 8 部分中已将 amqp_broker_url 属性添加到了咱们的 .yml 配置文件里面,因此这个步骤实际上已经作过了。

broker_url: amqp://guest:guest@192.168.99.100:5672 _(dev)_   
broker_url: amqp://guest:guest@rabbitmq:5672 _(test)_

注意,咱们在 “test” 配置文件里面填入的是 Swarm 服务名 “rabbitmq”,而不是从个人电脑上看到的 Swarm 的 LAN IP 地址。(你们的实际 IP 地址应该会有所不一样,不过运行 Docker Toolbox 时 192.168.99.100 彷佛是标准配置)。

咱们并不推荐在配置文件中填入用户名和密码的明文。在真实的使用环境中,咱们一般可使用在第 8 部分中看到的 Spring Cloud Config 服务器里面的内置加密功能。

单元测试

固然,咱们至少应该编写一个单元测试,以确保 handlers.go 中的 GetAccount 函数在有人请求由 “10000” 标识的很是特殊的账户时会尝试去发送一条消息。

为此,咱们须要在 handlers_test.go 中实现一个模拟的 IMessagingClient 还有一个新的测试用例。咱们先从模拟开始。这里咱们将使用第三方工具 mockery 生成一个 IMessagingClient 接口的模拟实现(在 shell 运行下面的命令的时候必定要先把 GOPATH 设置好):

> go get github.com/vektra/mockery/.../
> cd $GOPATH/src/github.com/callistaenterprise/goblog/common/messaging 
> ./$GOPATH/bin/mockery -all -output .
  Generating mock for: IMessagingClient

如今,在当前文件夹中就有了一个模拟实现文件 IMessagingClient.go。我看这个文件的名字不爽,也看不惯它的驼峰命名法,所以咱们将它重命名一下,让它的名字能更明显的表示它是一个模拟的实现,而且遵循本系列博客的文件名的一向风格:

mv IMessagingClient.go mockmessagingclient.go

咱们可能须要在生成的文件中稍微调整一下 import 语句,并删除一些别名。除此以外,咱们会对这个模拟实现采用一种黑盒方法 - 只假设它会在咱们开始测试的时候起做用。

不妨也看一看这里生成的模拟实现的源码,这跟咱们在第 4 章中手动编写的内容很是类似。

在 handlers_test.go 里添加一个新的测试用例:

// 声明一个模仿类来让测试更有可读性
var anyString = mock.AnythingOfType("string")
var anyByteArray = mock.AnythingOfType("[]uint8")  // == []byte
func TestNotificationIsSentForVIPAccount(t *testing.T) {
        // 配置 DBClient 的模拟实现
        mockRepo.On("QueryAccount", "10000").Return(model.Account{Id:"10000", Name:"Person_10000"}, nil)
        DBClient = mockRepo
        mockMessagingClient.On("PublishOnQueue", anyByteArray, anyString).Return(nil)
        MessagingClient = mockMessagingClient
        Convey("Given a HTTP req for a VIP account", t, func() {
                req := httptest.NewRequest("GET", "/accounts/10000", nil)
                resp := httptest.NewRecorder()
                Convey("When the request is handled by the Router", func() {
                        NewRouter().ServeHTTP(resp, req)
                        Convey("Then the response should be a 200 and the MessageClient should have been invoked", func() {
                                So(resp.Code, ShouldEqual, 200)
                                time.Sleep(time.Millisecond * 10)    // Sleep since the Assert below occurs in goroutine
                                So(mockMessagingClient.AssertNumberOfCalls(t, "PublishOnQueue", 1), ShouldBeTrue)
                        })
        })})
}

有关的详情都写在了注释里。在此,我也看不惯在断言 numberOfCalls 的后置状态以前人为地搞个 10 ms 的睡眠,但因为模拟是在与 “主线程” 分离的协程中调用的,咱们须要让它稍微挂起一段时间等待主线程完成一些工做。在此也但愿能对协程和管道(channel)有一个更好的惯用的单元测试方式。

我认可 - 使用这种测试方式的过程比在为 Java 应用程序编写单元测试用例时使用 Mockito 更加冗长。不过,我仍是认为它的可读性不错,写起来也很简单。

接着运行测试,并确保测试经过:

go test ./...

运行

首先要运行 springcloud.sh 脚原本更新配置服务器。而后运行 copyall.sh 并等待几秒钟,来让它完成对咱们的 “account service” 的更新。而后咱们再使用 curl 来获取咱们的 “特殊” 账户。

> curl http://$ManagerIP:6767/accounts/10000
{"id":"10000","name":"Person_0","servedBy":"10.255.0.11"}

若顺利的话,咱们应该可以打开 RabbitMQ 的管理控制台。而后再看看咱们是否在名为 vipQueue 的队列上收到了一条消息:

open http://192.168.99.100:15672/#/queues

img

在上图的最底部,咱们看到 “vipQueue” 有 1 条消息。咱们再调用一下 RabbitMQ 管理控制台中的 “Get Message” 函数,而后咱们应该能够看到这条消息:

img

在 Go 上编写消费者 - “vip service”

最后该从头开始写一个全新的微服务了,咱们将用它来展现如何使用 RabbitMQ 的消息。咱们会将迄今为止在本系列中学到的东西用到里面,其中包括:

  • HTTP 服务器
  • 性能监控
  • 集中配置
  • 重用消息传递机制代码

若是你执行过了 git checkout P9,那就应该能够在 root/goblog 文件夹中看到 “vipservice” 。

我不会在这里介绍每一行代码,毕竟它有些部分跟 “accountservice” 有所重复。咱们会将重点放在 咱们刚刚所发送的消息的 “消费方式” 上。有几点要注意:

  • 此时有两个新的 .yml 文件被添加到了 config-repo 里面。它们是 vipservice-dev.yml 和 vipservice-test.yml
  • copyall.sh 也有所更新,它会构建并部署 “accountservice” 和咱们新编写的 “vipservice”。

消费一条消息

咱们将使用 /goblog/common/messaging 和 SubscribeToQueue 函数中的代码,例如:

SubscribeToQueue(queueName string, consumerName string, handlerFunc func(amqp.Delivery)) error

对此咱们要提供这些参数:

  • 队列名称(例如 “vip_queue”)
  • 消费者名称
  • 一个收到响应队列的消息时调用的回调函数 - 就像咱们在第 8 章中消费配置的更新那样

将咱们的回调函数绑定到队列的 SubscribeToQueue 函数的实现也没什么好说的。这是其源代码,若是须要也能够看一看。

接下来咱们快速浏览一下 vip service 的 main.go 来看看咱们设置这些东西的过程:

var messagingClient messaging.IMessagingConsumer
func main() {
fmt.Println("Starting " + appName + "...")
config.LoadConfigurationFromBranch(viper.GetString("configServerUrl"), appName, viper.GetString("profile"), viper.GetString("configBranch"))
initializeMessaging()
// 确保在服务存在的时候关掉链接
handleSigterm(func() {
if messagingClient != nil {
messagingClient.Close()
}
})
service.StartWebServer(viper.GetString("server_port"))
}
// 在收到 "vipQueue" 发来的消息时会调用的回调函数
func onMessage(delivery amqp.Delivery) {
fmt.Printf("Got a message: %v\n", string(delivery.Body))
}
func initializeMessaging() {
        if !viper.IsSet("amqp_server_url") {
            panic("No 'broker_url' set in configuration, cannot start")
        }
        messagingClient = &messaging.MessagingClient{}
        messagingClient.ConnectToBroker(viper.GetString("amqp_server_url"))
        // Call the subscribe method with queue name and callback function
        err := messagingClient.SubscribeToQueue("vip_queue", appName, onMessage)
        failOnError(err, "Could not start subscribe to vip_queue")
        err = messagingClient.Subscribe(viper.GetString("config_event_bus"), "topic", appName, config.HandleRefreshEvent)
        failOnError(err, "Could not start subscribe to " + viper.GetString("config_event_bus") + " topic")
}

很熟悉对吧?咱们在后续的章节也极可能会反复提到设置并启动咱们加进去的微服务的方法。这也是基础知识的一部分。

这个 onMessage 函数只记录了咱们收到的任何 “VIP” 消息的正文。若是咱们要实现更多的仿真用例,就得引入一些花哨的逻辑来肯定帐户持有者是否有资格得到 “super-awesome buy all our stuff (tm)” 的待遇,而且也可能要往 “VIP offer 数据库“ 里写入记录。有兴趣的话不妨也实施一下这一逻辑,而后交个 pull request。

最后再提一下这段代码。在这段代码的帮助下,咱们能够按下 Ctrl + C 来杀掉一个服务的实例,或者咱们也能够等待 Docker Swarm 来杀死一个服务实例。

func handleSigterm(handleExit func()) {
           c := make(chan os.Signal, 1)
           signal.Notify(c, os.Interrupt)
           signal.Notify(c, syscall.SIGTERM)
           go func() {
                   <-c
                   handleExit()
                   os.Exit(1)
           }()
   }

这段代码在可读性上并不比别的代码好,它所作的只是将管道 “c” 注册为 os.Interruptsyscall.SIGTERM 的监听器,而且会阻塞性地监听 “c” 上的消息,直到接到任意信号为止。这使咱们可以确信,只要微服务的实例被杀了,这里的 handleExit() 函数就会被调用。若仍是不能确信的话,能够用 Ctrl + C 或者 Docker Swarm scaling 来测试一下。kill 指令也能够,不过 kill -9 就不行。所以,除非必须,最好不要用 kill -9 来结束任何东西的运行。

handleExit() 函数将调用咱们在 IMessageConsumer 接口上声明的 Close() 函数,该函数会确保 AMQP 链接的正常关闭。

部署并运行

这里的 copyall.sh 脚本已经更新过了。如有跟从上面的步骤,而且确保了合 Github 的 P9 分支的一致性,那就能够运行了。在完成部署以后,执行 docker service ls 就应该会打印这样的内容:

> docker service ls
ID            NAME            REPLICAS  IMAGE                        
kpb1j3mus3tn  accountservice  1/1       someprefix/accountservice                                                                            
n9xr7wm86do1  configserver    1/1       someprefix/configserver                                                                              
r6bhneq2u89c  rabbitmq        1/1       someprefix/rabbitmq                                                                                  
sy4t9cbf4upl  vipservice      1/1       someprefix/vipservice                                                                                
u1qcvxm2iqlr  viz             1/1       manomarks/visualizer:latest

或者也可使用 dvizz Docker Swarm 服务渲染器查看状态:

img

检查日志

因为 docker service logs 功能在 1.13.0 版本里面被标记成了实验性功能,所以咱们必须用老一套的方式来查看 “vipservice” 的日志。

首先,执行 docker ps 找出 CONTAINER ID:

> docker ps
CONTAINER ID        IMAGE                                                                                       
a39e6eca83b3        someprefix/vipservice:latest           
b66584ae73ba        someprefix/accountservice:latest        
d0074e1553c7        someprefix/configserver:latest

记下 vipservice 的 CONTAINER ID,并执行 docker logs -f 检查其日志:

> docker logs -f a39e6eca83b3
Starting vipservice...
2017/06/06 19:27:22 Declaring Queue ()
2017/06/06 19:27:22 declared Exchange, declaring Queue ()
2017/06/06 19:27:22 declared Queue (0 messages, 0 consumers), binding to Exchange (key 'springCloudBus')
Starting HTTP service at 6868

打开另外一个命令行窗口,并 curl 一下咱们的特殊帐户对象。

> curl http://$ManagerIP:6767/accounts/10000

若是一切正常,咱们应该在原始窗口的日志中看到响应队列的消息。

Got a message: {"accountId":"10000","readAt":"2017-02-15 20:06:27.033757223 +0000 UTC"}

工做队列

用于跨服务实例分发工做的模式利用了工做队列的概念。每一个 “vip 消息” 应该由一个“vipservice”实例处理。

img

因此让咱们看看使用 docker service scale 命令将 “vipservice” 扩展成两个实例时的情形:

> docker service scale vipservice=2

新的 “vipservice” 实例应该能在几秒钟内完成部署。

因为咱们在 AMQP 中使用了 direct / queue 的发送方法,咱们应该会见到一种轮转调度式(round-robin)的分发情形。再用 curl 触发四个 VIP 账户的查找:

> curl http://$ManagerIP:6767/accounts/10000
> curl http://$ManagerIP:6767/accounts/10000
> curl http://$ManagerIP:6767/accounts/10000
> curl http://$ManagerIP:6767/accounts/10000

再次检查咱们原始的 “vipservice” 的日志:

> docker logs -f a39e6eca83b3
Got a message: {"accountId":"10000","readAt":"2017-02-15 20:06:27.033757223 +0000 UTC"}
Got a message: {"accountId":"10000","readAt":"2017-02-15 20:06:29.073682324 +0000 UTC"}

正如咱们所预期的那样,咱们看到第一个实例处理了四个消息中的两个。若是咱们对另外一个 “vipservice” 实例也执行一次 docker logs,那么咱们也应该会在那里看到两条消息。

测试消费者

实际上,我并无真正想出一个好的方式来在避免花费大量的时间模拟一个 AMQP 库的前提下,对 AMQP 消费者进行单元测试。在 messagingclient_test.go 里面准备了一个测试,用于测试订阅者对传入消息的等待以及处理的一个循环。不过并无值得一提的地方。

为了更全面地测试消息传递机制,我可能会在后续的博客文章中回顾关于集成测试的话题。使用 Docker Remote API 或 Docker Compose 进行 go 测试。测试将启动一些服务,好比能让咱们在测试代码里面发送还有接收消息的 RabbitMQ。

足迹和性能

咱们此次不弄性能测试。在发送和接收一些消息以后快速浏览一下内存使用状况就足够了:

CONTAINER                                    CPU %               MEM USAGE / LIMIT
   vipservice.1.tt47bgnmhef82ajyd9s5hvzs1       0.00%               1.859MiB / 1.955GiB
   accountservice.1.w3l6okdqbqnqz62tg618szsoj   0.00%               3.434MiB / 1.955GiB
   rabbitmq.1.i2ixydimyleow0yivaw39xbom         0.51%               129.9MiB / 1.955GiB

上面即是在处理了几个请求以后的内存使用状况。新的 “vipservice” 并不像 “accountservice” 那么复杂,所以在启动后它应该会使用更少的内存。

概要

这大概是这个系列中最长的一部分了!咱们在这章完成了这些内容:

  • 更深刻地考察了 RabbitMQ 和 AMQP 协议。
  • 增长了全新的 “vipservice”。
  • 将与消息传递(和配置)有关的代码提取到了可重用的子项目中。
  • 基于 AMQP 协议发布 / 订阅消息。
  • 用 mockery 生成模拟代码。
问答
微服务架构:跨服务数据共享如何实现?
相关阅读
在微服务之间进行通讯 
RabbitMQ与AMQP协议
使用Akka HTTP构建微服务:CDC方法

此文已由做者受权腾讯云+社区发布,原文连接:https://cloud.tencent.com/dev...

欢迎你们前往腾讯云+社区或关注云加社区微信公众号(QcloudCommunity),第一时间获取更多海量技术实践干货哦~