基于Node.js和RabbitMQ搭建消息隊列

一.簡介

消息隊列
消息隊列(Message Queue,簡稱MQ),本質是個隊列,FIFO先入先出,只不過隊列中存放的內容是message而已。其主要用途:不同進程Process/線程Thread之間通信。使用消息隊列大概有以下原因:

  • 不同進程(process)之間傳遞消息時,兩個進程之間耦合程度過高,改動一個進程,引發(fā)必須修改另一個進程,為了隔離這兩個進程,在兩進程間抽離出一層(一個模塊),所有兩進程之間傳遞的消息,都必須通過消息隊列來傳遞,單獨修改某一個進程,不會影響另一個;
  • 不同進程(process)之間傳遞消息時,為了實現標準化,將消息的格式規(guī)范化了,并且,某一個進程接受的消息太多,一下子無法處理完,并且也有先后順序,必須對收到的消息進行排隊,因此誕生了事實上的消息隊列;

不管到底是什么原因催生了消息隊列,總之,上面兩個猜測是其實際應用的典型場景。綜上:

  • 消息隊列中的“消息”即指同一臺計算機的進程間,或不同計算機的進程間傳送的數據。
  • 消息隊列是在消息的傳輸過程中保存消息的容器。
  • 消息被發(fā)送到隊列中,消息隊列充當中間人,將消息從它的源中繼到它的目標。消息隊列可以保證在高并發(fā)狀態(tài)下數據入庫的順序性和準確性。

RabbitMq
RabbitMQ是實現AMQP(高級消息隊列協議)的消息中間件的一種。是應用比較廣泛和穩(wěn)定的成熟的消息隊列中間件。由于RabbitMQ是基于Erlang開發(fā)的,所以天生具有分布式優(yōu)點。

消息隊列應用場景
關于消息隊列的應用場景,可以先看這篇文章。這里我們主要模擬一個秒殺的場景。假定我們有100個商品的秒殺活動,現在我們需要保證前1000個發(fā)起請求的人能夠順利的記錄進入數據庫中。這個需求對于http方案是無法順利完成任務的,下面就需要消息隊列來完成。首先我們利用node.js和rabbitMQ搭建服務器,然后利用Siege模擬一個高并發(fā)的API請求??纯丛诟卟l(fā)請求下http方式和消息隊列方式的差異性和準確性。

二.應用

接下來我們就開始進行編碼環(huán)節(jié)。首先我們需要先安裝RabbitMQ,這里我們用之前說過的Docker來安裝。執(zhí)行如下命令。我的系統是Mac OS(懶得買服務器就在自己的機器上測試了),如果是linux的話會更好。

sudo docker pull rabbitmq 
#如果rabbitmq鏡像下載失敗,可以嘗試下載rabbitmq:management版本
或者 sudo docker pull rabbitmq:management
#然后用docker啟動rabbitmq
sudo docker run -d -e RABBITMQ_NODENAME=my-rabbit --name some-rabbit -p 5672:5672 rabbitmq:management

rabbit服務默認會啟動在5672端口,我們把他映射到宿主主機的5672端口。
然后我們需要安裝amqplib來在node中連接rabbitmq。我們創(chuàng)建好node的工作目錄,然后創(chuàng)建server.js。我們先來看看最簡單的消息隊列,也就是客戶端通過隊列把消息傳到服務端。在你的工程目錄下運行如下命令。

npm install amqplib

打開server.js,我們完成服務端的代碼。在這里我們用es5的promise來完成對回調函數的處理。如果不了解promise的可以先去看<a >ES6詳解。</a>

/**
 * Created by wsd on 17/2/23.
 */
var amqp = require('amqplib');
//首先我們需要通過amqp連接本地的rabbitmq服務,返回一個promise對象
amqp.connect('amqp://127.0.0.1').then(function(conn){
//進程檢測到終端輸入CTRL+C退出新號時,關閉RabbitMQ隊列。
  process.once('SIGN',function(){
    conn.close();
  });
//連接成功后創(chuàng)建通道
  return conn.createChannel().then(function(ch){
//通道創(chuàng)建成功后我們通過通道對象的assertQueue方法來監(jiān)聽hello隊列,并設置durable持久化為false。這里消息將會被保存在內存中。該方法會返回一個promise對象。
    var ok = ch.assertQueue('hello',{durable:false}).then(function(_qok){
//監(jiān)聽創(chuàng)建成功后,我們使用ch.consume創(chuàng)建一個消費者。指定消費hello隊列和處理函數,在這里我們簡單打印一句話。設置noAck為true表示不對消費結果做出回應。
//ch.consume會返回一個promise,這里我們把這個promise賦給ok。
      return ch.consume('hello',function(msg){
        console.log("[x] Received '%s'",msg.content.toString());
      },{noAck:true});
    });
//消費者監(jiān)聽完成之后,打印一行成功信息
    return ok.then(function(_consumeOk){
      console.log('[*] Waiting for message. To exit press CRTL+C');
    });
  });
}).then(null,console.warn);//如果報錯打印報錯信息

以上就是服務端的相關代碼,下面我們來看客戶端。創(chuàng)建client.js。我們還需要安裝when來運行promise。運行npm install when。

/**
 * Created by wsd on 17/2/23.
 */
var amqp = require('amqplib');
var when = require('when');
//連接本地消息隊列服務
amqp.connect('amqp://localhost').then(function(conn){
//創(chuàng)建通道,讓when立即執(zhí)行promise
  return when(conn.createChannel().then(function(ch){
    var q = 'hello';
    var msg = 'Hello World';
  //監(jiān)聽q隊列,設置持久化為false。
    return ch.assertQueue(q,{durable: false}).then(function(_qok){
  //監(jiān)聽成功后向隊列發(fā)送消息,這里我們就簡單發(fā)送一個字符串。發(fā)送完畢后關閉通道。
      ch.sendToQueue(q,new Buffer(msg));
      console.log(" [x] Sent '%s'",msg);
      return ch.close()
    });
  })).ensure(function(){ //ensure是promise.finally的別名,不管promise的狀態(tài)如何都會執(zhí)行的函數
//這里我們把連接關閉
    conn.close();
  });
}).then(null,console.warn);

接下來我們啟動服務和客戶端。

node server.js
#[*] Waiting for message. To exit press CRTL+C
node client.js
#[x] Sent 'Hello World'

然后我們切換到服務端

#[*] Waiting for message. To exit press CRTL+C
#[*] Received 'Hello World'!

至此一個最簡單的消息隊列搭建完成。下面我們來模擬文章一開始所說的秒殺的場景。我們會基于Http和RabbitMQ兩種實現形式做對比。
秒殺活動場景 http模擬
首先我們編寫服務模擬前端向server發(fā)起請求,這里我們采用koa框架來實現。新建http_web_server.js。

/**
 * Created by wsd on 17/2/23.
 */
var koa = require('koa');
//一個工具類
var util = require('util');
var route = require('koa-route');
var request = require('request');
//這個用于作為用戶id
var globalUserId = 1;
var app = koa()

//用于判斷服務是否啟動
app.use(route.get('/',function *(){
  this.body = 'Hello world';
}))
//定義請求到后端的URL地址,這里為了方便我就在本機上測試,大家如果有遠程服務器的話可以在遠程服務器上測試
var uri = 'http://127.0.0.1:8000/buy?userid=%d';
var timeout = 30 * 1000;//超時30s
//設置路由
app.use(route.get('/buy',function *(){
//用戶id簡單地每次請求遞增1
  var num = globalUserId ++;
//調用request發(fā)起請求
  request({
    method:'GET',
    timeout:timeout,
    uri:util.format(uri,num)
  },function(error,req_res,body){
    if(error){
      this.status = 500
      this.error = error
    }else if(req_res.status != 200){
      this.status = 500
    }else{
      this.body = body
    }
  })
}))
app.listen(5000,function(){
  console.log('server listen on 5000');
})

首先我們安裝koa,util,koa-route,request四個模塊。然后我們模擬向最終入庫的server發(fā)送生成訂單請求。接下來我們完成入庫server的相關代碼。由于我們需要對數據庫操作,所以需要安裝mongodb和mongoose模塊。

#安裝mongodb
brew intall mongodb
#啟動mongodb,設置數據的存儲路徑
mongod --dbpath data/db --logappend
#安裝mongoose
npm install mongoose

然后我們首先創(chuàng)建數據庫Model文件orderModel.js。

/**
 * Created by hwh on 17/2/23.
 */
var mongoose = require('mongoose');
//連接到本地開啟的mongodb,mongodb默認監(jiān)聽27017端口
var connstr = 'mongodb://127.0.0.1:27017/http_vs_rabbit';
//設置數據庫連接池大小
var poolsize = 50;
mongoose.connect(connstr,{server:{poolSize:poolsize}})
var Schema = mongoose.Schema;

var obj = {
  userId:{type:Number, required:true},
  writeTime:{type: Date,default: Date.now()}
}

var objSchema = new Schema(obj);
module.exports = mongoose.model('orders',objSchema);

然后我們創(chuàng)建數據庫操作文件orderLib.js。

/**
 * Created by hwh on 17/2/24.
 */
var objModel = require('./orderModel.js');
//針對generator的存取操作
exports.countAll = function(obj){
//獲得訂單總數
  return objModel.count()
}
exports.insertOneByObj = function(obj){
//創(chuàng)建訂單
  return objModel.create(obj);
}

//針對非generator的存取操作
exports.countAllNormal = function(obj,cb){
  return objModel.count(obj || {},cb)
}
exports.insertOneByObjNormal = function(obj,cb){
  return objModel.create(obj || {},cb)
}

最后我們創(chuàng)建http_back.js來接收數據并入庫。

/**
 * Created by hwh on 17/2/23.
 */
var koa = require('koa');
var route = require('koa-route');
var bodyparser = require('koa-bodyparser');
var app = koa();
var orderModel = require('./orderModellib.js');
var listenPort = 3000;

app.use(bodyparser())

app.use(route.get('/',function * (){
  this.body = "hello world,listenPort:" + listenPort
}));

app.use(route.get('/buy',function * (){
//拿到參數
  var userid = this.request.query.userid;
//獲取數據庫中訂單數量
  var count = yield orderModel.countAll();
//做判斷,大于100就不再入庫
  if (count > 100){
    this.body = 'sold out!';
  }else{
    var model = yield orderModel.insertOneByObj({
      userId:userid
    });
    if(model){
      this.body = 'success';
    }
  }
}));

app.listen(listenPort,function(){
  console.log('Server listening on:',3000);
})

這里由于我們需要對body進行解析,所以我們安裝koa-bodyparser模塊。代碼比較簡單。接下來我們安裝ngnix設置反向代理。

#安裝nginx
brew install nginx
#進入nginx目錄
cd /usr/local/etc/nginx
#修改配置文件
vi nginx.conf

我們主要設置反響代理相關配置。

#user  wsd;

#開啟兩個nginx進程,等于cpu核心數或者cpu*2
worker_processes  2;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    #事件模型,由于是mac系統使用kqueue;linux使用epoll。簡單說明一下兩種事件模型使用場景。Kqueue和Epoll都屬于高效事件模型。
    #Kqueue:使用于FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS       X.  使用雙處理器的MacOS X系統使用kqueue可能會造成內核崩潰。
    #Epoll:使用于Linux內核2.6版本及以后的系統。
    use kqueue;
    #單個工作進程的最大連接數,和硬件配置有關系。
    #盡量大但別超過CPU占用率的90%,這里我們?yōu)榱藴y試取值比較小。理論上每臺nginx服務器的最大連接數為worker_processes*worker_connections
    worker_connections  2048;
}

#設定http服務器,利用它的反向代理功能提供負載均衡支持
http {
  #設置請求數據格式,這里就使用mime支持的類型
    include       mime.types;
  #http content_type
    default_type  application/octet-stream;
  #暫不儲存日志(儲存日志需要先使用log_format指令設置日志格式)
    access_log off;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    #指定 nginx 是否調用sendfile 函數(zero copy 方式)來輸出文件,對于普通應用,必須設為on。如果用來進行下載等應用磁盤IO重負載應用,可設置為off,以平衡磁盤與網絡IO處理速度,降低系統uptime
    sendfile        on;

    #tcp_nopush     on;

    #keepalive_timeout  0;

    #設置超時時間
    keepalive_timeout  65;

    #定義負載均衡設備的Ip及設備狀態(tài)
    upstream backend {
      server 127.0.0.1:3000;
    }
    #gzip  on;

#配置ngnix啟動的地址
    server {
        listen       8000;
        server_name  localhost;#這里nginx啟動在本機8000端口

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

#匹配所有路徑
location / {
            proxy_pass http://backend;#設置負載均衡的地址,這里設置為為backend里面的地址
            
            proxy_redirect default;#設置返回客戶端請求頭的location的值,默認不設置
            proxy_http_version 1.1;#代理的http協議版本
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
    include servers/*;
}       

寫好配置文件以后我們保存然后啟動nginx。

#啟動nginx
nginx

我們訪問http://localhost:8000看看反向代理是否運行正常。如果正常的話會輸出server listen on 3000。

http測試
接下來我們就要對這個http服務進行壓力測試了。這里我們使用siege。

#安裝wget
brew install wget
#下載siege
wget http://download.joedog.org/siege/siege-latest.tar.gz
#解壓
tar -zxvf siege-latest.tar.gz
#進入siege腳本目錄
cd siege-4.0.2
#配置
./configure
#編譯并安裝
make && make install

可以輸入siege -help查看siege支持的命令。這里我們主要用到-c(指定并發(fā)數)和-r(指定測試次數)。現在我們分別模擬100、200、300個并發(fā),并循環(huán)發(fā)送10次。當然別忘記每次我們請求完畢后在下一次請求開始前要把數據庫清空。

siege -c 100 -r 10 -q http://192.168.1.150:5000/buy
siege -c 200 -r 10 -q http://192.168.1.150:5000/buy
siege -c 300 -r 10 -q http://192.168.1.150:5000/buy

每完成一次并發(fā)操作,我們使用mongo命令連接到本地的mongodb服務。并且查看數據庫里面訂單的數量。

#連接數據庫
mongo
#選擇數據庫
use http_vs_rabbit
#查看集合數量
 db.orders.count() 

下面是每一次并發(fā)操作后,數據庫中訂單的數量

//100次并發(fā)
114
//200次并發(fā)
125
//300次并發(fā)
119

可以看到,每一次并發(fā)訂單數量都會超出預訂值。下面是一些參數:

Date & Time            Trans    Elap Time     Data Trans  Resp Time  Trans Rate    Throughput    Concurrent  OKAY   Failed
2017-03-08 15:09:47,   1000,       3.62,           0,       0.01,      276.24,        0.00,        1.49,       0,       0
2017-03-08 15:10:48,   2000,       3.96,           0,       0.01,      505.05,        0.00,        3.10,       0,       0
2017-03-08 15:11:27,   3000,       4.19,           0,       0.01,      715.99,        0.00,        8.64,       0,       0

看一下上述各參數的意思。

  • Date & Time:請求時間
  • Trans:請求總數
  • Elap Time:測試用時
  • Data Trans:測試傳輸數據量
  • Resp Time:平均響應時間
  • Trans Rate:每秒事務處理量
  • Throughput:吞吐率
  • Concurrent:并發(fā)用戶數
  • OKAY:成功數
  • Failed:失敗數

可以看到在并發(fā)測試中,http處理事務的速度雖然不錯,但并不能保證結果的準確和可靠。下面我們來看一下利用rabbitmq的測試結果。首先我們也是像http一樣,寫一個服務模擬前端請求。這里我們新建rabbit_web_server.js。

/**
 * Created by wsd on 17/2/24.
 */
var koa = require('koa');
var router = require('koa-route');
var amqp = require('amqplib');
var uuid = require('node-uuid');
var app = koa();

var correlationId = uuid();
var q = 'fibq';//前端發(fā)送消息隊列
var q2 = 'ackq';//后臺回復隊列
//conn寫成全局變量,循環(huán)利用。否則每次訪問路由都會創(chuàng)建conn
var conn;
//依然id每次請求遞增1
var globalUserId = 1;

app.use(router.get('/',function * (){
  this.body = 'hello world';
}));

app.use(router.get('/buy/',function*(){
  var num = globalUserId ++;
  //conn我們在外部創(chuàng)建,并且只創(chuàng)建一次(復用)
  conn.createChannel().then(function(ch){
    //監(jiān)聽q2隊列(訂單量如果到達100,服務端會通過q2隊列返回信息)
    return ok = ch.assertQueue(q2,{durable:false}).then(function(){
      //創(chuàng)建消費q2隊列,這里簡單把信息設置到res的body里
      ch.consume(q2,function(msg){
        console.log(msg.content.toString());
        this.body = msg.content.toString();
        ch.close();
      },{noAck:true});
      //發(fā)送消息到q隊列,這里把訂單id作為content。把q2隊列的name和uuid也傳過去,這里uuid用來做消息的關聯id
      ch.sendToQueue(q,new Buffer(num.toString()),{replyTo:q2,correlationId:correlationId})
    });
  }).then(null,console.error);
}));

amqp.connect('amqp://127.0.0.1').then(function(_conn){
  conn = _conn;
});

app.listen(5001,function(){
  console.log('server listen on 5001');
});

下面我們在新建rabbit_mq_server.js文件,來寫入庫的操作。

/**
 * Created by hwh on 17/2/24.
 */

var amqp = require('amqplib');
var co = require('co');
var orderModel = require('./orderModellib');
var q = 'fibq';

amqp.connect('amqp://127.0.0.1').then(function(conn){
  process.once('SIGN',function(){
    conn.close();
  });
  return conn.createChannel().then(function(ch){
//設置公平調度,這里是指rabbitmq不會向一個繁忙的隊列推送超過1條消息。
    ch.prefetch(1);
    //定義回傳消息函數
    var ackSend = function(msg,content){
      //要注意這里我們之前傳上來的隊列名和uuid會被保存在msg對象的properties中
      //因為服務端并不知道回傳的隊列名字,所以我們需要把它帶過來
      ch.sendToQueue(msg.properties.replyTo,new Buffer(content.toString()),
          {correlationId:msg.properties.correlationId});
      //ack表示消息確認機制。這里我們告訴rabbitmq消息接收成功。
      ch.ack(msg);
    }
    //定義收到消息的處理函數
    var reply = function (msg){
      var userid = parseInt(msg.content.toString());
      //這里由于consume的處理函數不支持generator語法,這里我們就用es5的方式訪問數據庫、
      orderModel.countAllNormal({},function(err,count){
        if(count >= 100){
          return ackSend(msg,'sold out!');
        }else{
          orderModel.insertOneByObjNormal({
            userId:userid
          },function(err,model){
            return ackSend(msg,"buy success,orderid:"+model._id.toString())
          });
        }
      });
    };
    //監(jiān)聽隊列q并消費
    var ok = ch.assertQueue(q,{durable:false}).then(function(){
      ch.consume(q,reply,{noAck:false});
    });
    return ok.then(function(){
      console.log(' [*] waiting for message')
    })
  })
}).then(null,console.error);

分別啟動rabbit_web_server.js和rabbit_mq_server.js。接下來我們還是像之前測試http服務一樣,用siege模擬100、200、300次并發(fā)。要注意這里我們的服務變成了5001端口。
可以看到不管多少并發(fā)數下,我們數據庫里的訂單都是100。這保證了我們數據的準確性。下面是siege記錄的參數:

Date & Time            Trans    Elap Time     Data Trans  Resp Time  Trans Rate    Throughput    Concurrent  OKAY   Failed
2017-03-08 16:36:06,   1000,       3.81,           0,       0.01,      262.47,        0.00,        2.17,       0,       0
2017-03-08 16:40:50,   2000,       4.15,           0,       0.02,      481.93,        0.00,        9.16,       0,       0
2017-03-08 16:41:03,   3000,       4.47,           0,       0.08,      671.14,        0.00,       51.91,       0,       0

對比http,對于高并發(fā)的操作,確實隊列在耗時,每秒事務處理量和響應時間上會比http略遜一籌。由于node.js的異步I/O,所以http會存在插入超量的情況。因為很有可能你在異步往數據庫里面插入數據還沒有完成的時候,下一個請求已經過來了。但隊列保證了結果的準確性,這在秒殺場景以及一些特殊場景是硬性要求。這是一個很常見的場景,因此掌握消息隊列的操作是作為服務端開發(fā)來說必不可少的。

在這里,為了提升rabbitmq的性能,我們可以開啟多個rabbitmq進程。這個就交給大家下去測試吧。

rabbitmq還有以下幾種應用場景:

  • 一個生產者多個消費者
    • 輪詢


      Paste_Image.png
    • 廣播(faout)


      Paste_Image.png
    • 路由(direct)
Paste_Image.png
  • RPC遠程調用
Paste_Image.png
  • 跨平臺通信(比如node和python、java)

這些會在我后面的文章中講解,敬請期待。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發(fā)現,斷路器,智...
    卡卡羅2017閱讀 134,786評論 18 139
  • 來源 RabbitMQ是用Erlang實現的一個高并發(fā)高可靠AMQP消息隊列服務器。支持消息的持久化、事務、擁塞控...
    jiangmo閱讀 10,377評論 2 34
  • 1.RabbitMQ概述 簡介: MQ全稱為Message Queue,消息隊列是應用程序和應用程序之間的通信方法...
    梁朋舉閱讀 49,970評論 0 47
  • 關于消息隊列,從前年開始斷斷續(xù)續(xù)看了些資料,想寫很久了,但一直沒騰出空,近來分別碰到幾個朋友聊這塊的技術選型,是時...
    預流閱讀 585,255評論 51 786
  • 文‖云飛揚 當黃昏將至 你獨對雪山的夕陽 凄涼的眼眸 倒影著心愛姑娘的模樣 那燃盡的酥油燈 燒不盡你對她的想象 那...
    山東云飛揚閱讀 355評論 0 3