[英]使用CQRS模式创建Golang微服务

1,795 阅读4分钟
原文链接: outcrawl.com

Building a Microservices Application in Go Following the CQRS Pattern

This article walks through the development of a simplistic social network application where anyone can post anonymous messages.

Source code is available on GitHub.

Architecture

The application is built using the Command Query Responsibility Segregation (CQRS) pattern. The goal is to decouple command and query sides into separate services, where commands perform writing into the database and queries read eventually persistent data. The separation enables scaling the two sides independently, which is an advantage because normally there are more read operations than write operations. It also means we can have different data models for each service. The query side might return data in materialized views, which are created independently and asynchronously of the command side.

The application this article describes, is called Meower: a social network for cats.

Meower architecture

There are three services—Pusher, Meow and Query. Meow service handles the command side by exposing one HTTP POST endpoint for creating meows. Query service listens to events and inserts meows into Elasticsearch database. It exposes endpoints for reading meows ordered by time and performing full-text search. Pusher service sends newly created meows to clients through WebSockets.

Note that Meow and Query services are not strongly bounded, since they're using the same database. This goes against the whole idea, but it will make things a bit simpler. This is a development-only setup (there is no SSL, no replicas, all storage is ephemeral, etc.).

Prerequisites

If you haven't already, install Docker, Go and golang/dep dependency management tool.

Create a directory for your project inside $GOPATH.

Utilities

Starting off, create some utilities and a facade for communicating with third-party services.

Create util subdirectory and util/util.go file.

PostgreSQL

Create schema subdirectory and schema/model.go file.

Create db subdirectory and db/repository.go file.

This is a straightforward way of achieving inversion of control. By using Repository interface you allow any concrete implementation to be injected at runtime, and all function calls will be delegated to the impl object.

You can implement an in-memory database, which conforms to Repository interface, and use it during development and testing.

Create docker-compose.yaml file in your project's root directory and declare the postgres service in it.

Create postgres subdirectory and a postgres/up.sql file, which contains table definitions.

Create a postgres/Dockerfile file and copy postgres/up.sql file into the container as 1.sql.

SQL files inside /docker-entrypoint-initdb.d directory will be executed in alphabetical order.

Implement Repository interface for PostgreSQL database inside db/postgres.go file by using lib/pq package.

Meows are being ordered by their primary key, because keys will be k-sortable by time. This is to avoid introducing an additional index (more info).

NATS

Add nats service to docker-compose.yaml.

Create event subdirectory and event/messages.go file, which contains event message types.

Similarly as with database access pattern, declare utility functions for dealing with the event store inside event/event.go file.

Now implement EventStore interface for NATS in event/nats.go file.

Here are two different approaches of implementing the subscription functions. When consuming this API, use whichever one you prefer.

One way is to use a callback function.

Another way is to return a channel. Here, an intermediate channel is created to transform messages into appropriate type.

Elasticsearch

Update docker-compose.yaml file.

Create a repository interface inside search/repository.go file.

Implement the interface for Elasticsearch inside search/elastic.go by using olivere/elastic package.

Meow service

Create a meow-service subdirectory, and in meow-services/main.go file parse configuration from environment variables.

Connect to PostgreSQL and inject the repository. The code below retries connection every 2 seconds with tinrab/retry package.

Connect to NATS.

Finally, run the HTTP server.

The router binds POST /meows endpoint to the createMeowHandler handler function. Declare it inside meow-service/handlers.go file.

A new meow is created, inserted into the database, and an event is published.

Query service

Create query-service subdirectory and read the configuration variables inside query-service/main.go file.

Then connect to PostgreSQL, Elasticsearch and NATS.

Here, the query service is subscribed to OnMeowCreated event using onMeowCreated function.

Start the HTTP server.

Then in query-service/handlers.go, first declare onMeowCreated function to insert a meow into Elasticsearch whenever the OnMeowCreated event is received.

Write the searchMeowsHandler handler function, which performs full-text search and returns meows bounded with skip and take parameters.

Write the listMeowsHandler which returns all meows ordered by creation time.

Pusher service

Create pusher-service subdirectory.

Messages

Create pusher-service/messages.go file and declare messages to be send through WebSockets.

Client

Create pusher-service/client.go file and declare a struct representing a connected client.

Hub

Create pusher-service/hub.go file for the Hub struct which manages all clients.

Write the Run function.

Write functions for sending messages.

Write a function for upgrading HTTP requests to a WebSocket connection.

When a client gets connected, add it to the list.

When a client disconnects, remove it from the list.

Entry point

Create pusher-service/main.go file.

Docker image

Specify all services and dependencies between them in docker-compose.yaml file.

Create a Dockerfile file inside project's root directory. This image is built in two stages and contains binaries for all services. See Multi-Stage Docker Builds for Kubernetes for more information.

Reverse proxy

Reverse proxy will route traffic from and to the front-end app.

Update docker-compose.yaml file.

Create nginx subdirectory and nginx/Dockerfile file.

Write NGINX configuration inside nginx/nginx.conf file.

Front-end

Create a vue app using vue-cli 3.0+. Include Vuex when asked to select features.

Add all necessary dependencies.

Open main.js file and import Bootstrap SCSS files.

Modify store.js file.

Declare WebSocket mutations.

Declare mutations for updating meows.

Add an action for getting the meows timeline.

Add an action for creating meows.

Add an action for searching meows.

At the end of store.js file, dispatch the getMeows action to fetch meows when the page loads.

Meow component

Create a meow component inside src/components/Meow.vue file, which displays a single meow.

Timeline component

Declare the timeline component inside src/components/Timeline.vue file. It lists all meows and display a form for posting new ones.

Search component

Search component, declared inside src/components/Search.vue, is similar to the timeline. It calls the search endpoint every time the input changes.

App layout

Include search and timeline components inside src/App.vue.

Wrapping up

At this point everything should work as expected. To run the app, first build Docker images with Docker Compose and then start Vue development server.

This is how Meower looks like.

Finished app

Entire source code is available on GitHub.