node.js连接redis高可用性方案:Sentinel

node.js后台应用在开始时往往不会搞得太复杂,使用单实例的redis,一般都会使用官方推荐的模块 node_redis

访问单实例node.js

node_redis 的基础上稍作封装,主要是避免并行访问时意外创建多个redis连接。

database.js

var redis = require('redis');

function Database() {
    var self = this;

    self._redis_host = '127.0.0.1';
    self._redis_port = 6379;
    self._redis_db = 2;

    self._redis = null;
    self._redis_selected = false;
}

Database.prototype.redis = function(callback) {
    var self = this;

    if (self._redis && self._redis_selected) {
        return callback(null, self._redis);
    }

    if (! self._redis) {
        self._redis = redis.createClient(self._redis_port, self._redis_host);
    }

    self._redis.select(self._redis_db, function (err) {
        if (err) {
            return callback(err);
        }

        self._redis_selected = true;

        return callback(null, self._redis);
    });
};


module.exports = new Database();

client.js

使用 database.js

var db = require('./database');

db.redis(function (err, client) {
    if(err) {
        console.error(err.toString());
        return process.exit(1);
    }

    client.set('hello', 'world', function (err) {
        if(err) {
            console.error(err.toString());
            return process.exit(1);
        }

        setTimeout(function () {
            client.get('hello', function (err, value) {
                if(err) {
                    console.error(err.toString());
                    return process.exit(1);
                }

                console.log('hello: ' + value);

                process.exit(0);
            });
        }, 20*1000);
    });
});

访问主备Redis

但是随着服务的成功,用户量开始增加,另外对稳定性、可靠性有了一定要求,确保数据的安全性成了重中之重,redis由单机转向主/备(Replication)甚至集群(Cluster),本文只关注使用 Sentinel 管理的主/备(Replication)Redis。

这就意味着Redis客户端应用不能直接连接Redis实例,而是需要先连接 Sentinel ,根据 Sentinel 提供的 MasterSlave 地址连接Redis实例,还要接收 SentinelMaster Slave 变动通知,重连Redis实例。

不幸的是,node_redis 模块只支持单实例redis,基于 node_redis 实现与 Sentinel 的交互工作量比较大。

所幸的是 ioredis (现已成为redis官方推荐模块)出现了,它支持 Sentinel ,而且大部分API跟 node_redis 是兼容的:

ioredis is a robust, full-featured Redis client that is used in the world's biggest online commerce company Alibaba and many other awesome companies.

  1. Full-featured. It supports Cluster, Sentinel, Pipelining and of course Lua scripting & Pub/Sub (with the support of binary messages).
  2. High performance.
  3. Delightful API. It works with Node callbacks and Bluebird promises.
  4. Transformation of command arguments and replies.
  5. Transparent key prefixing.
  6. Abstraction for Lua scripting, allowing you to define custom commands.
  7. Support for binary data.
  8. Support for TLS.
  9. Support for offline queue and ready checking.
  1. Support for ES6 types, such as Map and Set.
  2. Support for GEO commands (Redis 3.2 Unstable).
  3. Sophisticated error handling strategy.

ioredis 提供了从 node_redis 进行迁移的文档 Migrating from node_redis

但也要注意 ioredis 的不同之处,如连接 Redis 失败时, node_redis 默认是不重连,而 ioredis 会重连,在 redis 故障时可能导致 node.js 积压大量请求耗尽内存。

参考文档 ioredis Sentinel ,将上一节 访问单实例node.js 中redis封装代码改成支持 Sentinel

database.js

var Redis = require('ioredis');

function Database() {
    var self = this;

    self._redis_options = {
        name: 'mymaster',
        sentinels: [{
            host: '127.0.0.1',
            port: 5000
        }, {
            host: '127.0.0.1',
            port: 5001
        }],
        db: 2
    };

    self._redis = null;
}

Database.prototype.redis = function(callback) {
    var self = this;

     if (! self._redis) {
        self._redis = new Redis(self._redis_options);
    }

    return callback(null, self._redis);
};

module.exports = new Database();

和使用 node_redisdatabase.js 对照一下,可以发现 ioredis 连接 redis 时,支持 db 选项,可以省去调用 select

值得注意的是 node_redis 最近开始支持连接 redis 时指定 db 选项,见 Parse redis url just like IANA · NodeRedis/node_redis@a4285c1 ,使用该特性时请安装最新版的 node_redis

db: null; If set, client will run redis select command on connect. This is not recommended.

引用自 redis.createClient()文档

启动 Redis 及 Sentinel

启动上一篇文章《 redis高可用性方案:Sentinel 》配置好的 Sentinel

  • Master redis-server ./redis-master.conf
  • Slave redis-server ./redis-slave.conf
  • Sentinel a redis-sentinel ./redis-sentinel-a.conf
  • Sentinel b redis-sentinel ./redis-sentinel-b.conf
  • Sentinel c redis-sentinel ./redis-sentinel-c.conf

演示运行

运行演示脚本

1: node client.js &
2: sleep 1
3: redis-cli -p 6380 -n 2 get hello
4: redis-cli -p 6379 debug sleep 30 &
1
运行 client.jsMaster 写入键值
2
等待 Master 同步数据到 Slave
3
Slave 读取键值
4
触发故障切换

演示脚本运行结果

5: "world"
6: OK
7: hello: world
5
演示脚本行 3 的redis命令执行结果
6
演示脚本行 3 的redis命令执行状态
7
演示脚本行 1 后台运行结束时输出的键值,此时由于主备已切换,是从新的 Master (原 Slave )上获取的