原创

分布式系统(一)-Redis

前言

Redis作为当前主流的缓存组件,合理的运用将极大提高系统的性能

说明

redis的优点,简单一个字就是
基于内存的存储让其吞吐量可以达到读的速度是110000次/s,写的速度是81000次/s(若使用SSD将更快)

持久化

  • RDB
    配置
    save 60000 1 #表示60000s内有1次修改 就进行保存

手动存储
执行命令bgsave

缺点 数据过大时 非常慢

  • AOF
    配置
    appendonly yes
    appendfilename "appendonly.aof"
    #appendfsync always
    appendfsync everysec
    # appendfsync no
    
    缺点 aof文件会非常大,但是可以保证数据不丢失

限流

通过lua脚本进行限流操作

local key=KEYS[1]; --key
local second = KEYS[2]; --单位s
local total=KEYS[3]; --并发总数


local count=redis.call("incr",key);
local time=redis.call("ttl",key);
local result=true;
if(time==-1)
then 
    redis.call("expire",key,second);
end
if(tonumber(count)>tonumber(total))
then
    result=false;
end
return tostring(result);

nodejs

var fs = require("fs");
var path = require("path");
var lua = "";//lua脚本
var sha = "";

var redis = require("redis");
var config = require("../../config");
var log = require("../log4");
var client;
var clientoption = { host: config.config.redis.host, port: config.config.redis.port, password: config.config.redis.password, db: 2 };
client = redis.createClient(clientoption);
client.on("ready", function (err) {
    // redis.debug_mode = true;
    if (err) {
        console.log(err);
    } else {
        // console.log("redis read ok");
        log.logger.info("redis read ok");
    }
});
client.on("error", function (err) {
    // console.log(err, "出异常了");
    log.logger.error(err);
})
var privatehandler = {
    //整体逻辑 
    flow: async function (key, second, total) {
        if (sha) {
            let d = await privatehandler.evalsha(sha, [3, key, second, total]);
            if (d == true) {
                return true;
            } else if (d == false) {
                return false;
            } else if (d == "NOSCRIPT") {
                if (!lua) {
                    throw "lua文件不存在";
                }
            }
        }
        if (!lua) {
            lua = fs.readFileSync(path.resolve(__dirname, 'limitflow.lua')).toString();
        }
        sha = await privatehandler.evalload(lua);
        return await privatehandler.flow(key, second, total);
    },
    //根据hash值执行一个脚本 如果脚本不存在 会返回NOSCRIPT
    evalsha: async function (sha, command) {
        return new Promise(function (resolve, reject) {
            if (!command.constructor === Array) {
                reject("请传入参数数组");
                return;
            }
            command.unshift(sha);
            command.unshift("evalsha");
            // console.log(command);
            client.multi([command]).exec(function (err, result) {
                // console.log(err,result);
                if (err) {
                    console.log(err);
                    reject(err);
                }
                else {
                    result = result[0];
                    if (result == "true") {
                        resolve(true);
                    } else if (result == "false") {
                        resolve(false);
                    } else if (result && result.code == "NOSCRIPT") {//没有读取到数据
                        resolve("NOSCRIPT");
                    } else {
                        reject(result);//其他异常
                    }
                }
            });
        })
    },
    evalload: async function (luacontent) {//加载lua脚本 返回一个sha
        return new Promise(function (resolve, reject) {
            client.multi([["script", "load", luacontent]]).exec(function (err, result) {
                if (err) {
                    console.log(err);
                    reject(err);
                }
                else {
                    result = result[0];
                    resolve(result);
                }
            });
        })
    }
}
//封装给外部调用
var handler = {
    /*
    key:关键key
    second:秒
    total:总数
    iswait:是否等待 如果不等待 将直接返回结果 如果等待 一直等到true
    */
    limitflow: async function (key, second, total, iswait) {
        if (!key || !second || !total || iswait == null) {
            throw "参数错误";
        }
        if (iswait) {
            while (!await privatehandler.flow(key, second, total)) {
                //如果不能继续执行 等10ms
                await sleep(10);
            }
            return true;
        } else {
            return await privatehandler.flow(key, second, total);
        }
    }
}

var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve();
        }, time);
    })
}

exports.redisflow = handler;

分布式锁

redis的分布式锁主要是通过nx命令来实现,利用redis的原子性操作,若存在相同的键,操作将会失败,同时利用redis的自动过期功能,可防止因意外导致的锁释放异常,不会阻塞后续的锁获取。

各语言实现的分布式锁大同小异,不在赘述,一般java项目使用redission来实现分布式锁

主从、集群、哨兵模式

主从

主从配置非常简单,只要在从库里配置上主库信息即可
slaveof 172.25.254.101 6379

哨兵模式

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

配置

首先配置Redis的主从服务器,修改redis.conf文件如下

# 使得Redis服务器可以跨网络访问
bind 0.0.0.0
# 设置密码
requirepass "123456"
# 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
slaveof 192.168.11.128 6379
# 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
masterauth 123456

上述内容主要是配置Redis服务器,从服务器比主服务器多一个slaveof的配置和密码。

配置3个哨兵,每个哨兵的配置都是一样的。在Redis安装目录下有一个sentinel.conf文件,copy一份进行修改

# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 192.168.11.128 6379 2
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456

上述关闭了保护模式,便于测试。

有了上述的修改,我们可以进入Redis的安装目录的src目录,通过下面的命令启动服务器和哨兵

# 启动Redis服务器进程
./redis-server ../redis.conf
# 启动哨兵进程
./redis-sentinel ../sentinel.conf

注意启动的顺序。首先是主机(192.168.11.128)的Redis服务进程,然后启动从机的服务进程,最后启动3个哨兵的服务进程。

集群

sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。
cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容。
集群特点

* 多个redis节点网络互联,数据共享

* 所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用

* 不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,
  并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为

* 支持在线增加、删除节点

* 客户端可以连接任何一个主节点进行读写
搭建

开启配置

#配置yes开启redis-cluster
cluster-enabled yes
#配置节点之间超时时间
cluster-node-timeout 15000
#这个配置很重要,cluster开启必须重命名指定cluster-config-file,不能与别的节点相同,否则会启动失败,最好按主机+端口命名
cluster-config-file nodes-6379.conf

创建集群

redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1

若需要修正槽位,使用官方命令

redis-cli --cluster fix

执行命令检查集群

redis-cli --cluster info 127.0.0.1:6382
正文到此结束
本文目录