1. 程式人生 > >node中的redis使用--ioredis

node中的redis使用--ioredis

nodejs 連結 redis/mysql 連線池

redis是單執行緒作業,所以不管查詢任務是由一個連結發來的還是多個連結發來的,redis是序列的執行。並通過當前的連結返回客戶端。nodejs接受redis的返回後,不管是不是並行,都要等主執行緒空閒下來才能一個個處理伺服器返回的資料。

再看mysql~

mysql不是單執行緒服務的,可以並行處理多個查詢請求。

mysql會為每一個連結建立一個單獨的執行緒查詢。redis資料基本在記憶體中,mysql會大量的讀取磁碟的I/O,多執行緒比較快。

但是nodejs是單執行緒的。但是它呼叫的I/O指令等是通過另外的執行緒做的,I/O指令完成後就給主執行緒小任務,回撥函式。

nodejs主執行緒一個,但是IO執行緒會有多個。

nodejs 使用多個連線來連線mysql。多連線是需要連線池的,有連線池就避免了每次連線都要去建立銷燬的消耗了。

綜上:nodejs + mysql用執行緒池是沒什麼問題的。nodejs + redis只用單個連線就夠。

所以有了連線管理模組,egg-redis。

redis效能

錯誤原因:redis client的業務程式碼以及redis client的I/O效能。

redis client採用的是單鏈接模式,底層採用的非阻塞網路I/O,

調優:pipeline,script

無依賴批量請求採用pipeline。

redis高效能體現在服務端處理能力,但瓶頸往往出現在客戶端,因此增強客戶端I/O能力與併發並行多客戶端才是高併發解決方案。

ioredis

效能為中心,功能齊全的,支援Redis >= 2.6.12 and (Node.js >= 6).

npm install ioredis

basic usage

var Redis = require('ioredis');
var redis = new Redis();
 
redis.set('foo', 'bar');
redis.get('foo', function (err, result) {
  console.
log(result); }); // Or using a promise if the last argument isn't a function redis.get('foo').then(function (result) { console.log(result); }); // Arguments to commands are flattened, so the following are the same: redis.sadd('set', 1, 3, 5, 7); redis.sadd('set', [1, 3, 5, 7]); // All arguments are passed directly to the redis server: redis.set('key', 100, 'EX', 10);

Connect to Redis

new Redis()       // Connect to 127.0.0.1:6379
new Redis(6380)   // 127.0.0.1:6380
new Redis(6379, '192.168.1.1')        // 192.168.1.1:6379
new Redis('/tmp/redis.sock')
new Redis({
  port: 6379,          // Redis port
  host: '127.0.0.1',   // Redis host
  family: 4,           // 4 (IPv4) or 6 (IPv6)
  password: 'auth',
  db: 0
})

一個特別的連線方法:

// Connect to 127.0.0.1:6380, db 4, using password "authpassword":
new Redis('redis://:[email protected]:6380/4')

pub/sub 釋出/訂閱

publish/subscribe

redis.on進行事件的監聽

var Redis = require('ioredis');
var redis = new Redis();
var pub = new Redis();
redis.subscribe('news', 'music', function (err, count) {
  // 現在我們訂閱了news和music兩個頻道
  // `count` 代表我們當前訂閱的序號
 
  pub.publish('news', 'Hello world!');
  pub.publish('music', 'Hello again!');
});
 
redis.on('message', function (channel, message) {
  // 收到訊息 Hello world! from channel news
  // 收到訊息 Hello again! from channel music
  console.log('Receive message %s from channel %s', message, channel);
});

// 還有一個是事件叫做`messageBuffer`,和message是一樣的
// 返回buffer,替代字串
redis.on('messageBuffer', function (channel, message) {
  // Both `channel` and `message` are buffers.
});

PSUBSCRIBE釋出

redis.psubscribe('pattern', function (err, count) {});
redis.on('pmessage', function (pattern, channel, message) {});
redis.on('pmessageBuffer', function (pattern, channel, message) {});

當客戶端發出訂閱或PSUBSCRIBE時,該連線將被放入“訂閱”模式。此時,只有修改訂閱集的命令才是有效的。當訂閱集為空時,連線將返回常規模式。

如果在訂閱模式下發送正常的命令,只會開啟一個新的連線。

處理二進位制資料:

redis.set('foo', Buffer.from('bar'));

拿到快取中的資料:

redis.getBuffer('foo', function (err, result) {
  // result is a buffer.
});

管道 Pipelining

傳送5條以上的命令,就可以使用管道的將命令放進記憶體的佇列中,然後一起傳送至redis,這個表現能提升百分之50-300.

這些命令在記憶體的佇列李阿敏,並配合exec方法進行執行。

var pipeline = redis.pipeline();
pipeline.set('foo', 'bar');
pipeline.del('cc');
pipeline.exec(function (err, results) {
  // `err` 總是null, and `results` 響應的陣列
  // corresponding to the sequence of queued commands.
  // Each response follows the format `[err, result]`.
});
 
// 甚至能夠鏈式呼叫命令
redis.pipeline().set('foo', 'bar').del('cc').exec(function (err, results) {
});
 
// `exec` 也是返回的promise
var promise = redis.pipeline().set('foo', 'bar').get('foo').exec();
promise.then(function (result) {
  // result === [[null, 'OK'], [null, 'bar']]
});

每個鏈式命令也可以有一個回撥,當命令得到回覆時將呼叫它:

redis.pipeline().set('foo', 'bar').get('foo', function (err, result) {
  // result === 'bar'
}).exec(function (err, result) {
  // result[1][1] === 'bar'
});

除了單獨向管道佇列新增命令外,還可以將一組命令和引數傳遞給建構函式:

redis.pipeline([
  ['set', 'foo', 'bar'],
  ['get', 'foo']
]).exec(function () { /* ... */ });

length的屬性:

const length = redis.pipeline().set('foo', 'bar').get('foo').length;
// length === 2

事務:

redis.multi().set('foo', 'bar').get('foo').exec(function (err, results) {
  // results === [[null, 'OK'], [null, 'bar']]
});

如果事務的命令鏈中存在語法錯誤(例如,引數數量錯誤,命令名稱錯誤等),則不會執行任何命令,並返回錯誤:

redis.multi().set('foo').set('foo', 'new value').exec(function (err, results) {
  // err:
  //  { [ReplyError: EXECABORT Transaction discarded because of previous errors.]
  //    name: 'ReplyError',
  //    message: 'EXECABORT Transaction discarded because of previous errors.',
  //    command: { name: 'exec', args: [] },
  //    previousErrors:
  //     [ { [ReplyError: ERR wrong number of arguments for 'set' command]
  //         name: 'ReplyError',
  //         message: 'ERR wrong number of arguments for \'set\' command',
  //         command: [Object] } ] }
});

就介面而言,multi與管道的不同之處在於,在為每個連結命令指定回撥時,排隊狀態將傳遞給回撥而不是命令的結果:

redis.multi({ pipeline: false });
redis.set('foo', 'bar');
redis.get('foo');
redis.exec(function (err, result) {
  // result === [[null, 'OK'], [null, 'bar']]
});
redis.multi([
  ['set', 'foo', 'bar'],
  ['get', 'foo']
]).exec(function () { /* ... */ });

管道支援內聯事務,這意味著您可以將管道中的命令子集分組到事務中:
redis.pipeline().get('foo').multi().set('foo', 'bar').get('foo').exec().get('foo').exec();

Lua指令碼

ioredis支援所有指令碼命令,例如EVAL,EVALSHA和SCRIPT。

ioredis公開了一個defineCommand方法,使指令碼編寫更容易使用:

var redis = new Redis();
 
// This will define a command echo:
redis.defineCommand('echo', {
  numberOfKeys: 2,
  lua: 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}'
});
 
// Now `echo` can be used just like any other ordinary command,
// and ioredis will try to use `EVALSHA` internally when possible for better performance.
redis.echo('k1', 'k2', 'a1', 'a2', function (err, result) {
  // result === ['k1', 'k2', 'a1', 'a2']
});
 
// `echoBuffer` is also defined automatically to return buffers instead of strings:
redis.echoBuffer('k1', 'k2', 'a1', 'a2', function (err, result) {
  // result[0] equals to Buffer.from('k1');
});
 
// And of course it works with pipeline:
redis.pipeline().set('foo', 'bar').echo('k1', 'k2', 'a1', 'a2').exec();

如果在定義命令時無法確定鍵的數量,則可以省略numberOfKeys屬性,並在呼叫命令時將鍵數作為第一個引數傳遞:

redis.defineCommand('echoDynamicKeyNumber', {
  lua: 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}'
});
 
// Now you have to pass the number of keys as the first argument every time
// you invoke the `echoDynamicKeyNumber` command:
redis.echoDynamicKeyNumber(2, 'k1', 'k2', 'a1', 'a2', function (err, result) {
  // result === ['k1', 'k2', 'a1', 'a2']
});

Redis規範

cluster建構函式接受兩個引數,

  • 第一個引數是連線到的叢集節點列表,不需要列舉所有的叢集節點,但是如果一個節點無法訪問客戶端將會嘗試下一個節點,並且客戶端將至少連線一個節點時自動發現其他節點。

  • 第二個引數是選項

    • clusterRetryStrategy:當沒有啟動任何節點可訪問時,clusterRetryStrategy將呼叫,返回一個數字,ioredis將嘗試在指定的延遲(毫秒為單位)後從頭開始重新連線到啟動節點,否則將返回無啟動節點可用錯誤。
    // 選項的預設值
    function (times) {
      var delay = Math.min(100 + times * 2, 2000);
      return delay;
    }
    
    • enableOfflineQueue:類似於類的enableOfflineQueue選項Redis
    • enableReadyCheck:啟用後,只有在cluster info報告叢集準備好處理命令的命令時才會發出“就緒”事件。否則,它將在“connect”發出後立即發出。
    • scaleReads:配置傳送讀取查詢的位置。
    • maxRedirections:當收到與群集相關的錯誤(例如MOVED,等)時,客戶端將命令重定向到另一個節點。此選項限制傳送命令時允許的最大重定向。預設值為。ASK``CLUSTERDOWN``16
    • retryDelayOnFailover:如果在傳送命令時目標節點斷開連線,ioredis將在指定的延遲後重試。預設值為100。您應該確保retryDelayOnFailover * maxRedirections > cluster-node-timeout 在故障轉移期間確保沒有命令失敗。
    • retryDelayOnClusterDown:當群集關閉時,所有命令都將被拒絕,錯誤為CLUSTERDOWN。如果此選項是數字(預設情況下為100),則客戶端將在指定時間(以毫秒為單位)後重新發送命令
    • retryDelayOnTryAgain:如果此選項是一個數字(預設情況下是100),則客戶端將TRYAGAIN在指定時間(以毫秒為單位)後重新發送被拒絕的命令。
    • redisOptionsRedis連線到節點時傳遞給建構函式的預設選項。
    • slotsRefreshTimeout:從群集重新整理插槽時發生超時前的毫秒數(預設值1000
    • slotsRefreshInterval:每個自動插槽重新整理之間的毫秒數(預設5000

    叢集模式下的釋出和訂閱:(和獨立模式下的釋出訂閱一致)

    sub.on('message', function (channel, message) {
      console.log(channel, message);
    });
     
    sub.subscribe('news', function () {
      pub.publish('news', 'highlights');
    });