如何使用 spring boot&47;spring cloud实现rpc 实现微服务应用

Spring Boot和Spring Cloud实现微服务架构学习(四)-Spring Cloud总结热门博客列表 -
ITeye博客频道 -
博客专栏推荐
本月博客排行
年度博客排行
最新文章列表
相关知识库
36714关注/3149收录
3278关注/393收录
15572关注/208收录
32510关注/3748收录
博客人气排行榜
博客电子书下载排行5820人阅读
微服务及云计算学习总结(20)
上篇主要是讲解理论知识和项目架构要点,这篇将集中在微服务中使用Spring Boot、Spring Cloud和Project Reactor实现事件溯源的原始主题。文章中也会介绍项目实现一些技术细节,项目Git下载地址:/kbastani/spring-cloud-event-sourcing-example,项目我自己已经运行了一遍,非常适合学习使用。下面是原文翻译内容:
Project Reactor
Project Reactor 是一个开源基于JVM实现Reactive流规范的开发框架,是Spring生态系统一个成员,在微服务中,经常在一个上下文下需要和其他微服务交互操作,由于微服务架构天然属性是最终一致性,而最终一致性并不保证数据的安全性。它提供我们一个使用异步非堵塞方式进行通讯的方式,这里正是使用Reactor目的所在。
很少情况下,领域模型状态会被跨微服务共享,但是如果在微服务之间需要共享状态怎么办?或者说多个微服务需要访问同一个数据库数据表怎么办?在微服务中ES只保存有序事件的日志,使用事件流取代领域模型在数据库中存储,我们可以存储有序事件流来代表对象的状态,这样,意味著我们就不再使用基于HTTP的RESTful进行微服务之间同步通讯,这些同步会造成堵塞和性能延迟。
Reactor为核心的事件溯源
在网上商店的微服务之一是购物车服务(Shopping Cart Service),已验证的用户从商店Web应用程序的用户界面浏览产品目录。用户可以添加和删除他们的购物车的商品条目,以及清除他们的购物车或结帐。
一个用户的购物车为如何实现事件追溯工作描绘了一个简单的样例。我们以网上商店中购物车服务为案例,展示Reactor + ES是如何实现的:
购物车服务Shopping Cart Service是一个MYSQL数据库拥有者,有一个数据表称为cart_event。这个表包含用户操作动作产生的有序事件日志,用户操作就是反复将商品加入购物车或去除等各种购物车管理操作。
Example 1. CartEventType.java
// These will be the events that are stored in the event log for a cart
public enum CartEventType {
REMOVE_ITEM,
CLEAR_CART,
CartEventType是枚举类型,已经列出了4种不同的事件类型。这些事件类型中的每一个都代表用户在购物车上执行的动作。根据ES,这些购物车事件可以影响用户的购物车的最终状态结果。当用户添加或删除一个商品条码到他们的购物车时,一个动作产生一个事件,会对购物车中进行递增或递减一行条目。当这些事件使用同样顺序进行回放时,同样一系列的条目会被重新创建或删除:
id created_at last_modified cart_event_type product_id quantity user_id
1 5 5 0 SKU-
2 8 8 1 SKU-
3 4 4 0 SKU-
4 2 2 0 SKU-
5 7 7 1 SKU-我们看到每行都有一个唯一时间戳来确保严格顺序,使用整数来代表4个购物车事件类型,product_id 和数据quantity都是每次加入购物车的商品条码信息。
这一结果显示在上面的截图,在这里,我们看到一个用户的购物车,生成作为一个总结果集的对象。
选择事件存储库
当选择事件追溯的适当存储选项时,有很多可用的选项。今天几乎所有的数据库都能提供数据流查询功能的工作,然而,有一些流行的开源项目,在这方面有着突出的优点。现在Event Sourcing标准的存储库是 Apache Kafka,微服务之间共享状态是通过共享Kafka的事件日志实现的,这是一个未来博客的主题。在这个例子中我们将使用MySQL,这是实现事件追溯一个在线购物车的不错选择。
您的事件存储技术的选择将永远取决于写入的数量和您的数据库的吞吐量。像Apache Kafka设计了精确的使用情况,却要求我们承担一些额外的工作责任去在生产中扩展,包括运行Apache ZooKeeper集群。
下面我们回到购物车,购物车微服务提供一个REST API方法接受来自Web端的事件。Web端发出事件的控制器 ShoppingCartControllerV1.java
Example 2. ShoppingCartControllerV1.java
@RequestMapping(path = &/events&, method = RequestMethod.POST)
public ResponseEntity addCartEvent(@RequestBody CartEvent cartEvent) throws Exception {
return Optional.ofNullable(shoppingCartService.addCartEvent(cartEvent))
.map(event -& new ResponseEntity(HttpStatus.NO_CONTENT))
.orElseThrow(() -& new Exception(&Could not find shopping cart&));
}在上面的代码示例,我们定义了一个用于收集来自客户端新的CartEvent对象的控制器方法。这种方法的目的是在向事件日志追加事件。当客户端调用REST API检索用户的购物车,它将产生一个购物车聚合,使用Reactive流合并了所有购物车事件流。
下面在ShoppingCartServiceV1.java中使用Reactor产生购物车事件流:
Example 3. ShoppingCartServiceV1.java
public ShoppingCart aggregateCartEvents(User user, Catalog catalog) throws Exception {
// Create a reactive streams publisher by streaming ordered events from the database
Flux&CartEvent& cartEvents =
Flux.fromStream(cartEventRepository.getCartEventStreamByUser(user.getId()));
// Aggregate the current state of the shopping cart until arriving at a terminal state in the stream
ShoppingCart shoppingCart = cartEvents
.takeWhile(cartEvent -& !ShoppingCart.isTerminal(cartEvent.getCartEventType()))
.reduceWith(() -& new ShoppingCart(catalog), ShoppingCart::incorporate)
// Generate the list of line items in the cart from the aggregate
shoppingCart.getLineItems();
return shoppingC
在上面的代码示例中,我们可以看到三个步骤来生成购物车对象,然后返回到客户端。第一步是从事件存储的数据源中创建一个Reactive流。一旦流建立,我们可以从事件流中产生我们的聚合。这些事件流不断改变购物车状态直至到最终状态,然后就可以将最终购物返回给用户客户端。
为了减少reactive流的聚合,我们使用了一个称为incorporate方法,这个方法是接受CartEvent对象,而CartEvent对象是用来改变购物车状态的。
Example 4. ShoppingCart.java
public ShoppingCart incorporate(CartEvent cartEvent) {
// Remember that thing about safety properties in microservices?
Flux&CartEventType& validCartEventTypes =
Flux.fromStream(Stream.of(CartEventType.ADD_ITEM,
CartEventType.REMOVE_ITEM));
// The CartEvent's type must be either ADD_ITEM or REMOVE_ITEM
if (validCartEventTypes.exists(cartEventType -&
cartEvent.getCartEventType().equals(cartEventType)).get()) {
// Update the aggregate view of each line item's quantity from the event type
productMap.put(cartEvent.getProductId(),
productMap.getOrDefault(cartEvent.getProductId(), 0) +
(cartEvent.getQuantity() * (cartEvent.getCartEventType()
.equals(CartEventType.ADD_ITEM) ? 1 : -1)));
// Return the updated state of the aggregate to the reactive stream's reduce method
}在上面代码中我们看到ShoppingCart的incorporate方法实现,我们接受一个CartEvent对象然后做最重要的一件事:确保事件类型是正确的。这是在微服务需要自由与他们的单元测试,在最终一致的架构,以确保状态突变将确保数据的正确性。在本例中,我们确保事件类型是 ADD_ITEM 或 REMOVE_ITEM。
下一步是更新购物车中每个条目的聚合视图,通过映射相应的事件类型到商品条目的数量递增或递减。最后我们返回这样一个带有最终可变状态的购物车给客户端。
Docker Compose演示
示例项目使用Docker Compose构建和运行,每个微服务的容器镜像都作为Maven编译过程的一部分。
首先,访问该示例项目的GitHub库:
/kbastani/spring-cloud-event-sourcing-example
克隆或复制项目并将存储库下载到您的机器上。下载后,您将需要使用Maven和Docker来编译和构建本地镜像。
下载Docker
首先,如果你还没有下载Docker。可遵循这里的指示/docker-toolbox,把Docker toolbox在你的开发机器上运行。
在你安装了Docker toolbox以后,运行下面的命令来初始化这个示例应用程序的一个新的VirtualBox虚拟机。
$ docker-machine create env event-source-demo --driver virtualbox --virtualbox-memory &11000& --virtualbox-disk-size &100000&
$ eval &$(docker-machine env event-source-demo)&
能够运行实例程序,需要在你的开发机上安装下面的软件:
Maven 3Java 8DockerDocker Compose
通过命令行方式来构建当前项目,在项目的根目录中运行如下的命令:
$ sh run.sh该项目将下载所有所需的依赖关系,并编译每一个项目的组件。每个服务都将被构建,然后一个Maven Docker插件会自动构建每个镜像到你的本地Docker注册表里。在根目录运行命令之前,一定要保证Docker正常运行,只有这样,你的sh run.sh命令才会构建成功。
Docker Compose启动集群
现在,每个镜像都已经搭建成功,我们可以使用Docker Compose快速启动集群。run.sh脚本将建立每个项目和Docker容器,并使用Docker Compose开启每个服务。值得注意的是,集群需要首先启动的服务是配置服务和发现服务。其余的服务在开始启动,并最终开始相互沟通。
我强烈建议你运行此示例的机器上至少16GB的系统内存。
一旦启动序列完成后,你可以浏览到Eureka主机和看到发现服务里注册的所有服务。复制并粘贴以下命令到终端,Docker可以使用$DOCKER_HOST环境变量进行访问。
$ open $(echo \&$(echo $DOCKER_HOST)\&|
\sed 's/tcp:\/\//http:\/\//g'|
\sed 's/[0-9]\{4,\}/8761/g'|
\sed 's/\&//g')如果Eureka正确的启动,浏览器将会启动并打开Eureka服务的仪表盘,如下图所示:
当所有的应用程序在Eureka上已经完成启动和注册,你可以使用以下命令访问网上商店的Web应用。
$ open $(echo \&$(echo $DOCKER_HOST)\&|
\sed 's/tcp:\/\//http:\/\//g'|
\sed 's/[0-9]\{4,\}/8787/g'|
\sed 's/\&//g')
应用程序启动可能需要一些时间,所以请确保你每隔几分钟刷新用户界面,直到看见产品目录。
要开始向购物车添加产品,您需要登录默认用户。单击Login按钮,你将被重定向到身份验证网关页面,在此页面你可以使用用户的默认凭据和密码进行登录。
登录成功后,你将被重定向到需要身份验证才能呈现的主页面上,并可以开始管理你的购物车中的项目。
在这篇文章中,我们很难看到在微服务架构中的高可用性和数据一致性的挑战。我们期待一个完整的网上商店原生云应用程序,能作为一系列微服务的集合,使用事件追溯保持一致的世界观,同时还保证高可用性。
在接下来的博客,我将继续探索如何在事件追溯中使用Spring Cloud Stream和用Apache Kafka对事件流处理。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:384561次
积分:5006
积分:5006
排名:第5600名
原创:178篇
转载:27篇
评论:45条
文章:14篇
阅读:149243
(2)(2)(4)(1)(4)(4)(13)(7)(4)(6)(1)(1)(2)(1)(1)(5)(3)(2)(3)(5)(20)(3)(27)(23)(23)(11)(15)(1)(9)(3)(1)(1)6072人阅读
开发环境搭建
AngularJS2的开发需要node.js环境。在Windows下面安装node.js,最好使用NVM(Node Version Manager)。在这里可以找到NVM的详细信息:
安装好NVM之后,用nvm install latest命令,会自动安装最新版本的node.js以及npm。另外还需要在系统的环境变量PATH里面添加node的路径,已方便使用。
建立一个portal目录,并且添加package.json,tsconfig.json,type.json,systemjs.config.js。这些都是AngularJS2项目标准的配置文件,详细信息请参考AngularJS2的快速开始文档:,只需要修改一下package.json里面的name就可以了。之后在cmd,或者git
bash里面,进入portal目录,运行npm install,npm会自动安装AngularJS2所需要的所有依赖包。其它为文件和目录也按照这个快速开始文档建立,包括一个index.html,一个app目录。另外portal下面有ponent.html,app.module.ts,app.routing.ts,main.ts,这些都是每个AngularJS2项目都有的文件。TS文件使用了TypeScript语言,是Angular推荐的语言。其中app.routing.ts定义了网站的路由,具体路由的使用信息请参考:。代码里面有一个地方值得提一下,即ponent.html这个文件,实际上做了我们这个单页应用的主视图。所有其它routing里面的视图,都是嵌入到该文件的router-outlet标签的。该标签实际上是一个Angular2的指令,告诉router应该在那里显示视图。具体请参考:。我们具体看一下AngularJS2调用上节中建立的user-service的代码。这些基本的代码不再一一列出,请参考github上面的代码:,注意不要使用master
HEAD的代码,而是用链接里面这个tag的代码。
shared/user.ts
import { Links } from './links';
export class User {
    constructor (
        public username: string = null,
        public password: string = null,
        public _links: Links = null,
    ) { }
TypeScript中,public为默认访问级别,所以不加private就是public的。user.ts导出了User类。与后台的User类相比,少了id属性。前面说过id默认是不导出的,这里遵循这个默认,并且添加了一个_links属性。
/shared/user.service.ts
import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions, Response } from '@angular/http';
import { User } from './user';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class UserService {
constructor (
private http: Http
private url = 'http://localhost:8080/user'
getAllUsers(): Promise&User[]& {
return this.http.get(this.url)
.toPromise()
.then(this.extraData)
.catch(this.handleError);
private extraData(response: Response) {
return response.json()._embedded['user'];
private handleError (error: any) {
let msg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'unknown error';
console.error(msg); // log to console instead
return Promise.reject(msg);
Angular同样使用了MVC设计模式。所以我们在文件命名中,可以看到.module.ts,.component.ts,.component.html,.service.ts。按照AngularJS的命名习惯,调用后台RESTful API的代码应该放在service里面,并且在需要用到该service的module里面,用@NgModule注释的providers注入。这样该module对应的Component就可以使用了。请参考下面的home.module.ts,ponent.ts,注意UserService类并没有被显式的实例化,而只是在home.module.ts用@NgModule注释进行了依赖注入,然后在HomeModule的构造里面声明了一个private的UserService的成员。另外可以依赖注入的组件,需要用@Injectable注释声明,就像UserService类那样。依赖注入的管理是AngularJS2的一个重要功能。
之后HomeComponent用getAllUsers方法发送GET请求到后台服务,然后解析返回的JSON数据并解析出user数组。
home.module.ts
import { NgModule }
from '@angular/core';
import { CommonModule }
from '@angular/common';
import { UserService }
from '../shared/user.service';
import { HomeComponent }
from './ponent';
@NgModule({
imports: [
CommonModule,
declarations: [
HomeComponent,
providers: [
UserService
export class HomeModule { }
<ponent.ts
import { Component, OnInit, OnDestroy } from &#39;@angular/core&#39;;
import { Router, ActivatedRoute } from &#39;@angular/router&#39;;
import { User } from &#39;../shared/user&#39;;
import { UserService } from &#39;../shared/user.service&#39;;
import { Subscription } from &#39;rxjs/Subscription&#39; ;
@Component({
templateUrl: &#39;/app/ponent.html&#39;
export class HomeComponent implements OnInit, OnDestroy {
subscription: S
users: User[];
constructor(
private router: Router,
private route: ActivatedRoute,
private userService: UserService,
ngOnInit() {
this.userService.getAllUsers().then(
users =& {
console.log(users);
this.users =
error =& console.error(error)
ngOnDestroy() {
this.subscription.unsubscribe();
templateURL指明了对应的视图。HomeComponent调用UserService的getAllUsers取得全部user,然后通过视图表现出来。视图大部分的代码跟标准HTML没有区别。AngularJS2最重要的特性之一就是支持双向绑定。目前ponent.html的代码使用的{{ }}只是单向绑定,即从模型到视图。双向绑定的列子我们放到后面。{{ }}相当于ngModel指令。ngFor指令实现了for循环。注意前面的*不能省略。
<ponent.html
&div class=&container&&
&h2&Welcome to Healtrav&/h2&
&div class=&container&&
&h3&All users&/h3&
&li *ngFor=&let user of users&&
&h5&username: {{ user.username }}&/h5&
&h5&password: {{ user.password }}&/h5&
&h5&_links.self.href: {{ user._links.self.href }}&/h5&
该页面显示了系统中所有的用户。
在portal目录下用npm start命令运行,会自动弹出系统默认浏览器,并且打开链接http://localhost:3000
如果是IE浏览器,用户会自动显示出来。如果是chrome浏览器,会发现没有用户显示出来。按F12,会看到如下的错误提示:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
这是因为Spring默认的HTTP Response,不会添加Access-Control-Allow-Origin这个HTTP头。其中一个解决方案是配置Spring,添加这个头,请参考Spring的官方文档:。另外一个解决方案将在下一章讲。
使用Angular-CLI
对于新建项目,一个更好的方式是使用Angular-CLI。只需要几行命令,就可以生成项目的基础文件,减少了开发人员的工作。另外还可以直接打包发布。最新版本的CLI没有使用SystemJS,而是改为用Webpack。Webpack是一个前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。使用CLI的基本步骤:
npm install -g angular-cli
产生新项目并运行:
ng new PROJECT_NAME
cd PROJECT_NAME
ng serve打开网页http://localhost:4200/即可以访问。
产生component:
ng g component my-new-component其它的service,module,等等,都可以用类&#20284;的命令产生。
ng build之后在dist目录下面产生打包文件。
打包为产品:
ng build --prod --aot
打包时制定index.html的base标签:
ng build --bh /myUrl/
运行测试:
推荐大家使用CLI工具建立和开发项目。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:23155次
排名:千里之外
(1)(1)(4)(2)&&&& Spring cloud适应于云端服务,也适用于企业信息化SOA建设。spring boot也是restful微服务开发的利器。但对于内网服务,即服务与服务之间的调用,spring并没有去刻意封装,也许他们认为已经没有必要了,因为已经有了thrift、ice等强大的框架。
&&&&如果是用spring boot本身提供的restful服务作为服务与服务之间的调用,效率低很多,thrift的效率大概是restful的100-1000倍左右。本篇既是基于spring boot框架,结合thrift和zookeeper实现的一个简单微服务框架,服务与服务之间使用thrift通信(thrift既是通信方式也是数据压缩方式)。
&&&&本demo一共包括三个工程:
&&&&cloud-thrift-server:服务提供方
&&&&cloud-thrift-interface:接口及传输对象定义
&&&&cloud-thrift-client:服务调用方
&&&&开源代码地址:
1)建立thrift接口定义文档
&&&&namespace java cloud.simple.service
&&&&struct UserDto {
&&&&&&1: i32 id
&&&&&&2: string username
&&&&&service UserService {
&&&&&&UserDto getUser()
&&&&接口定义完后,使用thrift命令生成对应的java文件,主要生成两个文件,分别是UserService.java和UserDto.java,把这两个文件放入cloud-thrift-interface工程,因为客户端也需要这个接口定义。
2)实现thrift服务注册
&&&&在服务的提供端需要实现接口,并且还要把实现类注册到thrift服务器。
&&&&UserService.Processor processor = new UserService.Processor(
&&&&&&&&&&&&&&&&&&&&&& new UserServiceImpl());
&&&&&&&& TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(
&&&&&&&&&&&&&&&&&&&&&& tServerTransport()).processor(processor));
&&&&UserServiceImpl就是接口实现类,将其注册金Tserver。
&&&&注册完服务后,需要启动Tserver,很显然这个需要在线程里启动。
&&&&executor.execute(new Runnable() {
&&&&&&&&&&&&&&& @Override
&&&&&&&&&&&&&&& public void run() {
&&&&&&&&&&&&&&&&&&&&&& tServer().serve();&
&&&&&&&&&&&&&&& }
3)&使用zookeeper进行服务名称注册
&&&&上面是注册具体的服务执行类,这一步是将服务的实例注册进zookeeper,这样才能实现负载均衡。让客户端可以根据服务实例列表选择服务来执行。当然这里只需要注册服务所在服务器的IP即可,因为客户端只要知道IP,也就知道访问那个IP下的该服务。
&&&&String servicePath = "/"+serviceN// 根节点路径
&&&&&& ZkClient zkClient = new ZkClient(serverList);
&&&&&& boolean rootExists = zkClient.exists(servicePath);
&&&&&& if (!rootExists) {
&&&&&&&&&& zkClient.createPersistent(servicePath);
&&&&&& InetAddress addr =
&&&&&& try {
&&&&&&&&&& addr = InetAddress.getLocalHost();
&&&&&& } catch (UnknownHostException e) {
&&&&&&&&&& e.printStackTrace();
&&&&&& String ip = addr.getHostAddress().toString();
&&&&&& String serviceInstance = System.nanoTime() +"-"+
&&&&&& // 注册当前服务
&&&&&& zkClient.createEphemeral(servicePath + "/" + serviceInstance);
&&&&&& System.out.println("提供的服务为:" + servicePath + "/" + serviceInstance);
&&&&要注意这里使用zkClient.createEphemeral建立临时节点,如果这台服务器宕机,这个临时节点是会被清除的,这样客户端在访问时就不会再选择该服务器上的服务。
4)&客户端更新服务列表
&&&&客户端需要能及时的监听服务列表的变化并作出负载均衡,我们用如下方式监听服务列表的变化:
&&&&// 注册事件监听
&&&&&&&& zkClient.subscribeChildChanges(servicePath, new IZkChildListener() {
&&&&&&&&&&&&&&& // @Override
&&&&&&&&&&&&&&& public void handleChildChange(String parentPath,
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& List&String& currentChilds) throws Exception {
&&&&&&&&&&&&&&&&&&&&&& // 实例(path)列表:当某个服务实例宕机,实例列表内会减去该实例
&&&&&&&&&&&&&&&&&&&&&& for (String instanceName : currentChilds) {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& // 没有该服务,建立该服务
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& if (!serviceMap.containsKey(instanceName)) {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& serviceMap.put(instanceName,createUserService(instanceName));
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&&&&& for (Map.Entry&String, UserService.Client& entry : serviceMap.entrySet()) {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& // 该服务已被移除
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& if (!currentChilds.contains(entry.getKey())) {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& serviceMap.remove(entry.getKey());
&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&&&&& System.out.println(parentPath + "事件触发");
&&&&&&&&&&&&&&& }
&&&&有了服务列表,客户端在调用服务的时候就可以采用负载均衡的方式了,在这里使用最简单的随机方式:
&&&&&&&& public UserService.Client getBalanceUserService(){&&&&&&&
&&&&&&&& Map&String, UserService.Client& serviceMap =ZooKeeperConfig.serviceM
&&&&&&&& //以负载均衡的方式获取服务实例&&&&&&&&&&&
&&&&&&&& for (Map.Entry&String, UserService.Client& entry : serviceMap.entrySet()) {
&&&&&&&&&&&&&&& System.out.println("可供选择服务:"+entry.getKey());
&&&&&&&& }
&&&&&&&& int rand=new Random().nextInt(serviceMap.size());&&&&&&&&&&&
&&&&&&&& String[] mkeys = serviceMap.keySet().toArray(new String[serviceMap.size()]);
&&&&&&&& return serviceMap.get(mkeys[rand]);
本文结束,具体参见代码,另外,之前还不了解thrift和zookeeper的朋友,不要被他们吓到,其实他们是很轻量级的技术,很容易上手,这也许就是他们流行的原因。
阅读(...) 评论()

我要回帖

更多关于 spring cloud项目实现 的文章

 

随机推荐