Redis两台服务器间实现主从自动切换高可用

前言

Redis一般情况下的部署方案

方案优点缺点
单机部署简单、只需一台服务器当该服务器出现故障则无法提供Redis服务
哨兵模式当无法连接的Redis节点小于一半(不包含一半)时任然可以提供服务需要3台及以上服务器
集群模式包含哨兵模式的优点,又具备负载均衡的特性包含哨兵模式的缺点,且不具备在线扩展的能力,部署难度也相对提高

那么有没有一种类似于mysql的互为主从的模式,仅需要2台服务器就能实现高可用的方案呢?

借鉴了网络上其他方案,并砍去了keepalived,采用crontab定时任务+iptables定时任务的方式

前期准备

服务器资源
服务器IP系统版本
服务器A192.168.88.102Ubuntu22
服务器B192.168.88.103Ubuntu22
程序资源
名称版本下载地址包类型备注
Redis7.0.5http://download.redis.io/releases/redis-7.0.5.tar.gz源码包
nginx1.22.1https://nginx.org/download/nginx-1.22.1.tar.gz源码包

安装reids

A、B服务器操作一致

解压缩

# 将安装包上次到服务器后,切换目录至安装包同级目录

# 解压

tar -zxvf redis-7.0.5.tar.gz

安装编译环境

# Ubuntu

apt-get install make gcc pkg-config libc6-dev



# CentOS

# yum -y install gcc automake autoconf libtool make

编译

# 切换到源码根目录

cd redis-7.0.5



# 编译

make



# 安装

make PREFIX=/usr/local/redis install



# 将默认配置文件复制到安装目录

cp redis.conf /usr/local/redis/



# 如编译失败后,再次编译前请先清除残留文件

# Ubuntu

# make distclean



# CentOS

# make clean

修改配置文件

# 切换目录

cd /usr/local/redis/



# 编辑文件

vi redis.conf



# 修改内容如下:
# 允许其他设备远程连接redis

bind 0.0.0.0



# 允许后台启动

daemonize yes



# 修改日志存放目录,不需要日志可改为logfile "/dev/null"

logfile "/var/log/redis_6379.log"


# 数据持久化目录

dir /usr/local/redis/



# 设置密码,如:

requirepass D83544E45CA39C7653BF21612FAD0FD1

启动和测试

# 启动

./bin/redis-server redis.conf



# 尝试连接 -a 后接的是密码

./bin/redis-cli -a D83544E45CA39C7653BF21612FAD0FD1



# 设置一个变量a,值为1

set a 1

# 查询变量a的值

get a

# 退出

quit

停止

# D83544E45CA39C7653BF21612FAD0FD1为密码

./bin/redis-cli -a D83544E45CA39C7653BF21612FAD0FD1 shutdown

设置防火墙

Ubuntu22

# 若未启用可用如下命令启动防火墙

ufw enable

# 允许所有外来访问以tcp协议连接6379端口

ufw allow 6379/tcp

CentOS7

# 若未启用可用如下命令启动防火墙

systemctl start firewalld

# 允许所有外来访问以tcp协议连接6379端口

firewall-cmd --znotallow=public --add-port=80/tcp --permanent

# 重新加载防火墙规则

firewall-cmd --reload

配置主从

将redis.conf文件复制两份分别为slave.conf和master.conf

cp redis.conf slave.conf

cp redis.conf master.conf

分别对两台服务器的slave.conf文件做如下修改

A服务器slave.conf

# 设置B服务器为主服务

replicaof 192.168.88.103 6379



# 配置连接密码,即主服务的密码

masterauth D83544E45CA39C7653BF21612FAD0FD1

B服务器slave.conf

# 设置A服务器为主服务

replicaof 192.168.88.102 6379



# 配置连接密码,即主服务的密码

masterauth D83544E45CA39C7653BF21612FAD0FD1

启动验证

A服务器以主身份启动

./bin/redis-server master.conf

B服务器以从身份启动

./bin/redis-server slave.conf

分别连接两台服务器的Redis

# 连接A

./bin/redis-cli -h 192.168.88.102 -a D83544E45CA39C7653BF21612FAD0FD1


# 连接B

./bin/redis-cli -h 192.168.88.103 -a D83544E45CA39C7653BF21612FAD0FD1

在A中设置变量y 值为3

set y 3

在B中查询变量y 得到值为3

get y

证明主从设置成功,注意主服务提供查和写功能,但从服务只提供查功能


实现主从切换

A、B服务器操作一致

在redis根目录上创建redis.sh文件

vi /usr/loacl/redis/redis.sh

写入如下内容

#!/bin/bash

# Redis 启动脚本
#

# 脚本当前目录
PRG="$0"
while [ -h "$PRG" ]; do
  ls=`ls -ld "$PRG"`
  link=`expr "$ls" : '.*-> \(.*\)$'`
  if expr "$link" : '/.*' > /dev/null; then
    PRG="$link"
  else
    PRG=`dirname "$PRG"`/"$link"
  fi
done
PRGDIR=`dirname "$PRG"`
SH_DIR=`cd "$PRGDIR" >/dev/null; pwd`

APP_NAME="redis-server"
# 根路径
#APP_PATH=/usr/local/redis
APP_PATH=$SH_DIR
BIN_PATH=$APP_PATH/bin
# 配置文件路径
CONF_PATH=$APP_PATH/
CONF_FILE_PATH=$CONF_PATH/redis.conf
MASTER_CONF_FILE_PATH=$CONF_PATH/master.conf
SLAVE_CONF_FILE_PATH=$CONF_PATH/slave.conf

# 运行端口 - 默认 6379
APP_PORT=6379
# Redis 密码 - 默认为空
APP_PASS="D83544E45CA39C7653BF21612FAD0FD1"
## 从配置文件,读取端口、密码
PORT_STR=$(cat $CONF_FILE_PATH | grep -E '^port'| sed 's/port \([0-9]*\).*/\1/g' )
PASS_STR=$(cat $CONF_FILE_PATH | grep -E '^requirepass' | sed 's/.*requirepass \(.*\)/\1/g' | sed 's/"//g')

#echo "PORT_STR: $PORT_STR"
#echo "PASS_STR: $PASS_STR"
if [ -n "$PORT_STR" ]; then
  APP_PORT=$PORT_STR;
fi
if [ -n "$PASS_STR" ]; then
  APP_PASS=$PASS_STR;
fi
# 处理密码连接字符串
PASS_KEY=""
if [ -n "$APP_PASS" ]; then
  PASS_KEY="-a $APP_PASS"
fi
echo -e "\e[35m======================================================================\e[0m"
#使用说明 用来提示参数
usage(){
  echo -e "\e[33mAPP_PATH:\e[0m \e[36m $APP_PATH \e[0m"
  echo -e "\e[33mUsage:\e[0m    \e[36m $0 [start|stop|restart] \e[0m"
  echo -e "\e[33mUsage:\e[0m    \e[36m $0 [status|running|role|slaveup] \e[0m"
  echo -e "\e[33mUsage:\e[0m    \e[36m $0 [tomaster|toslave|startmaster|startslave|startauto] \e[0m"
  echo -e "\e[35m----------------------------------------------------------------------\e[0m"
  echo -e " -\e[32m start   \e[0m: 正常启动 Redis,$BIN_PATH/redis-server $CONF_FILE_PATH"
  echo -e " -\e[32m stop    \e[0m: 停止 Redis, $BIN_PATH/redis-cli shutdown"
  echo -e " -\e[32m restart \e[0m: 重启 Redis, stop && start"
  echo -e "\e[35m----------------------------------------------------------------------\e[0m"
  echo -e " -\e[32m status   \e[0m: 当前运行状态、对应角色类型"
  echo -e " -\e[32m running  \e[0m: 当前运行状态,运行中返回 0,否则返回 1"
  echo -e " -\e[32m role     \e[0m: 当前运行对应角色类型,master 返回 0, slave 返回 1, 其它 返回2, redis 未运行 返回9"
  echo -e " -\e[32m slaveup  \e[0m: 当前运行状态为slave时,master_link_status是否为up (是则返回0,否则为1)"
  echo -e " -\e[32m masterup \e[0m: 判断另一个节点当前角色 (master则返回0,slave则为1)"
  echo -e "\e[35m----------------------------------------------------------------------\e[0m"
  echo -e " -\e[32m tomaster   \e[0m: 运行状态转换角色类型为【master】"
  echo -e " -\e[32m toslave    \e[0m: 运行状态转换角色类型为【slave】"
  echo -e " -\e[32m startmaster\e[0m: 未运行时,以【master】角色类型运行"
  echo -e " -\e[32m startslave \e[0m: 未运行时,以【slave】角色类型运行"
  echo -e " -\e[32m startauto  \e[0m: 未运行时,根据slave.conf的master节点状态,自动判断当前节点以【master/slave】角色类型运行"
  echo ""
  exit 1
}
#检查程序是否已运行
is_running(){
  # 根据应用名称,获取进程pid (当存在多个时不适用)
  pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}'`

  # 根据端口号获取对应 pid
  #pid=$(lsof -t -i:$APP_PORT)
  #pid=$(lsof -i:$APP_PORT | grep *: | awk '{print $2}')
  #如果不存在返回1 存在返回0
  if [ -z "${pid}" ]; then
   return 1
  else
   return 0
  fi
}
# 启动方法
start(){
  is_running
  if [ $? -eq "0" ]; then
    echo "$APP_NAME - [PORT: $APP_PORT] is already running . pid=${pid}"
  else
    $BIN_PATH/redis-server $CONF_FILE_PATH
    sleep 1
    status
    if [ $? -eq "0" ]; then
      echo "$APP_NAME - [PORT: $APP_PORT] start success . pid=${pid}"
    else
      echo "$APP_NAME - [PORT: $APP_PORT] start fail ."
    fi
  fi
}
# 用 master.conf 的配置文件替换 redis.conf 后启动
startmaster(){
  echo "准备以【 master 】模式启动,复制替换 redis.conf 文件..."
  cp -f $MASTER_CONF_FILE_PATH $CONF_FILE_PATH
  sleep 1
  start
}

# 用 slave.conf 的配置文件替换 redis.conf 后启动
startslave(){
  echo "准备以【 slave 】模式启动,复制替换 redis.conf 文件..."
  cp -f $SLAVE_CONF_FILE_PATH $CONF_FILE_PATH
  sleep 1
  start
}
# 自动判断当前启用 master 还是 slave 模式
startauto(){
  is_running
  if [ $? -eq "0" ]; then
    echo "${APP_NAME} - [PORT: ${APP_PORT}] is already running . pid=${pid}"
    exit 1
  fi

  # 读取 slave.conf 配置,获取对应 master 配置

  arr=(`cat ${SLAVE_CONF_FILE_PATH} | grep ^replicaof`)
  master_host=${arr[1]}
  master_port=${arr[2]}
  authcmd=$(cat $SLAVE_CONF_FILE_PATH | grep ^masterauth | sed 's/masterauth //g' | sed 's/"//g')
  auth_key=""

  if [ -n "$authcmd" ]; then
    auth_key="-a $authcmd"
  fi
  echo "设置 master -h $master_host -p $master_port $auth_key "
  # 判断 master 配置中的服务,对应角色类型
  roleName=$($BIN_PATH/redis-cli -h $master_host -p $master_port $auth_key info | grep role: | sed 's/.$//g')

  echo "slave 配置的 master 当前类型: $roleName"
  if [ "$roleName" = "role:master" ]; then
    startslave
  else
    startmaster
  fi
}
# 自动判断另一个节点 master 还是 slave 模式
masterup(){

  # 读取 slave.conf 配置,获取对应 master 配置
  arr=(`cat ${SLAVE_CONF_FILE_PATH} | grep ^replicaof`)
  master_host=${arr[1]}
  master_port=${arr[2]}
  authcmd=$(cat $SLAVE_CONF_FILE_PATH | grep ^masterauth | sed 's/masterauth //g' | sed 's/"//g')
  auth_key=""

  if [ -n "$authcmd" ]; then
    auth_key="-a $authcmd"
  fi
  echo "设置 master -h $master_host -p $master_port $auth_key "
  # 判断 master 配置中的服务,对应角色类型
  roleName=$($BIN_PATH/redis-cli -h $master_host -p $master_port $auth_key info | grep role: | sed 's/.$//g')

  echo "slave 配置的 master 当前类型: $roleName"
  if [ "$roleName" = "role:master" ]; then
    return 0
  else
    return 1
  fi
}


# 停止方法
stop(){
  is_running
  if [ $? -eq "0" ]; then
    echo "$APP_NAME - [PORT: $APP_PORT] is running . pid=${pid} ; stoping..."
    # 执行 shutdown
    $BIN_PATH/redis-cli $PASS_KEY shutdown

    sleep 1
    is_running
    if [ $? -eq "0" ]; then
      echo "$APP_NAME - [PORT: $APP_PORT] stop fail . pid=${pid}"
    else
      echo "$APP_NAME - [PORT: $APP_PORT] stop success ."
    fi
  else
    echo "$APP_NAME - [PORT: $APP_PORT] is not running ."
  fi
}

#重启
restart(){
  stop
  start
}

# 判断运行的角色[ master, slave ]
# master 返回 0, slave 返回 1, 其它 返回2, redis 未运行 返回9
role(){
  is_running
  if [ $? -eq "0" ]; then
    roleName=$($BIN_PATH/redis-cli $PASS_KEY info | grep role: | sed 's/.$//g')
    if [ "$roleName" = "role:master" ]; then
      return 0
    elif [ "$roleName" = "role:slave" ]; then
      return 1
    else
      echo "ERROR: unknow role: [$roleName] !!!"
      return 2
    fi
  else
    echo "$APP_NAME - [PORT: $APP_PORT] is not running ."
    return 9
  fi
}

# 判断当前运行的 是否为 slave 且 正常连接上 master
# 为简化逻辑,此处实现仅 在 info 信息中匹配 master_link_status:up (运行正常返回0,否则为1)
# 即正常调用前,应该要知道此节点正常运行 且 为 slave
slaveup(){
  linkStatus=$($BIN_PATH/redis-cli $PASS_KEY info | grep master_link_status:up)
  if [ -z "$linkStatus" ]; then
   #echo "redis master link is down"
   return 1
  else
   #echo "redis master link is up"
   return 0
  fi
}

# Change Redis to master; Use Command:  replicaof no one
tomaster(){
  role
  mode=$?
  if [ $mode -eq "0" ]; then
    echo "$APP_NAME - [PORT: $APP_PORT] 已经是[ master ] 不必要切换 ."
  elif [ $mode -eq "1" ]; then
    # 切换配置文件 - 避免下次重启后变化
    cp -f $MASTER_CONF_FILE_PATH $CONF_FILE_PATH
    # 执行切换为 主服务
    $BIN_PATH/redis-cli $PASS_KEY replicaof no one
    sleep 1
    status
  else
    echo "$APP_NAME - [PORT: $APP_PORT] 不支持操作状态 [$mode]."
  fi
}

# Change Redis to slave; Use Command:  replicaof masterip masterport
toslave(){
  role
  mode=$?
  if [ $mode -eq "1" ]; then
    echo "$APP_NAME - [PORT: $APP_PORT] 已经是[ slave ] 不必要切换 ."
  elif [ $mode -eq "0" ]; then
    # 切换配置文件 - 避免下次重启后变化
    cp -f $SLAVE_CONF_FILE_PATH $CONF_FILE_PATH

    authcmd=$(cat $CONF_FILE_PATH | grep ^masterauth | sed 's/"//g')
    echo "设置 master 连接密码. $authcmd"
    # 执行更新密码
    $BIN_PATH/redis-cli $PASS_KEY config set $authcmd

    repcmd=$(cat $CONF_FILE_PATH | grep ^replicaof)
    echo "准备切换为[ slave ], $repcmd"
    # 执行切换为 从服务
    $BIN_PATH/redis-cli $PASS_KEY $repcmd

    sleep 1
    status
  else
    echo "$APP_NAME - [PORT: $APP_PORT] 不支持操作状态 [$mode]."
  fi
}

# 判断状态
status(){
  is_running
  if [ $? -eq "0" ]; then
    echo "$APP_NAME - [PORT: $APP_PORT] is running . pid=${pid}"
    role
    if [ $? -eq "0" ]; then
      echo "Redis is master role"
    elif [ $? -eq "1" ]; then
      echo "Redis is slave role"
    else
      echo "Redis is unknow role"
    fi
  else
    echo "$APP_NAME - [PORT: $APP_PORT] is not running ."
  fi
}

# 根据输入的参数执行对应的方法
case "$1" in
  "start")
    start
    ;;
  "stop")
    stop
    ;;
  "running")
    is_running
    ;;
  "status")
    status
    ;;
  "role")
    role
    ;;
  "restart")
    restart
    ;;
  "tomaster")
    tomaster
    ;;
  "toslave")
    toslave
    ;;
  "startmaster")
    startmaster
    ;;
  "startslave")
    startslave
    ;;
  "startauto")
    startauto
    ;;
  "slaveup")
    slaveup
    ;;
  "masterup")
    masterup
    ;;
  *)
    usage
    ;;
esac

赋予可执行权限

chmod +x redis.sh

启动测试

A、B服务器分别重启服务

./redis.sh restart

B服务器将从服务切换为主

./redis.sh tomaster

A服务将主服务改为从

./redis.sh toslave

分别连接2个Redis应用

B服务器连接B服务并设置一个变量t 值为2

A服务器连接B服务并查找t变量,得到值2


自动切换主从

A、B服务器操作一致

创建检测脚本

vi /usr/local/redis/vip_keepalived.sh

写入如下内容:

#!/bin/bash

BASE_PATH=/usr/local/redis
# 转发规则生效
iptables_up(){
while ([ `iptables -t nat -nL PREROUTING --line | grep 6379$ | grep 6378 | wc -l` -gt 1 ])
do
        iptables -t nat -D PREROUTING -p tcp --dport 6378 -j REDIRECT --to-port 6379
done

while ([ `iptables -t nat -nL OUTPUT --line | grep 6379$ | grep 6378 | wc -l` -gt 1 ])
do
        iptables -t nat -D OUTPUT -p tcp --dport 6378 -j REDIRECT --to-port 6379
done

if [ `iptables -t nat -nL PREROUTING --line | grep 6379$ | grep 6378 | wc -l` -eq "0" ]; then
        iptables -t nat -A PREROUTING -p tcp --dport 6378 -j REDIRECT --to-port 6379
fi

if [ `iptables -t nat -nL OUTPUT --line | grep 6379$ | grep 6378 | wc -l` -eq "0" ]; then
        iptables -t nat -A OUTPUT -p tcp --dport 6378 -j REDIRECT --to-port 6379
fi
}

# 删除转发规则
iptables_down(){
while([ `iptables -t nat -nL PREROUTING --line | grep 6379$ | grep 6378 | wc -l` -gt 0 ])
do
        iptables -t nat -D PREROUTING -p tcp --dport 6378 -j REDIRECT --to-port 6379
done

while([ `iptables -t nat -nL OUTPUT --line | grep 6379$ | grep 6378 | wc -l` -gt 0 ])
do
        iptables -t nat -D OUTPUT -p tcp --dport 6378 -j REDIRECT --to-port 6379
done
}

# 检查是否切换角色
check(){
        # 判断运行的角色[ master, slave ] master 返回 0, slave 返回 1, 其它 返回2, redis 未运行 返回9
        $BASE_PATH/redis.sh role
        redisRole=$?
        echo "当前 redis role值: [$redisRole]"
        if [ $redisRole -eq "0" ]; then
                iptables_up
                # redis 有运行,且为 master 主节点,返回 0
                $BASE_PATH/redis.sh masterup
                if [ $? -eq "0" ]; then
                        $BASE_PATH/redis.sh toslave
                        iptables_down
                fi
                return 0
        elif [ $redisRole -eq "1" ]; then
                iptables_down
                #echo " redis 有运行,且为 slave 状态"
                # 判断 master_link_status 是否为 up
                $BASE_PATH/redis.sh slaveup
                if [ $? -eq "0" ]; then
                        #echo "slave 连接 master 正常" 
                        return 1;
                else
                        # 判断 另一个节点的role,只有另一个节点不为master才能将本节点切换为master
                        $BASE_PATH/redis.sh masterup
                        if [ ! $? -eq "0" ]; then
                                # slave 连接 master 已断开,修改本节点为 master
                                $BASE_PATH/redis.sh tomaster
                                iptables_up
                                return 0
                        else

                                iptables_down
                                return 1
                        fi
                fi
        else
                iptables_down
                #echo " 其它状态,直接返回对应结果[$redisRole]"
                return $redisRole
        fi
        # 删除转发规则
        iptables_down
        #echo "redis 未运行,直接返回 1"
        return 1
}
check

赋予可执行权限

chmod +x /usr/local/redis/vip_keepalived.sh

创建定时任务

echo "* * * * * /usr/local/redis/vip_keepalived.sh > /tmp/check_redis.log"| crontab

安装nginx

可参考以往安装,此处简略描述

注意安装时加入 --with-stream

#安装nginx核心模块
tar -zxvf nginx-1.12.0.tar.gz
cd nginx-1.12.0
# 注意加入tcp代理的模块即可
./configure --with-stream
make
make install
chmod a+rwx -R /usr/local/nginx/logs/
chmod a+rwx -R /usr/local/nginx/
/usr/local/nginx/sbin/nginx  -t
/usr/local/nginx/sbin/nginx
cat /usr/local/nginx/logs/error.log

# 设置开机自启动
cp $PRGDIR/nginx /etc/init.d/
chmod +x /etc/init.d/nginx
chkconfig --add nginx
chkconfig nginx on

通过nginx实现高可用

在nginx配置文件末尾加入:

stream {
  
  upstream backend-redis {
# A服务器redis映射端口
    server 192.168.88.102:6378;
# B服务器redis映射端口
    server 192.168.88.103:6378;
  }
  
  server {
#代理端口 6380 程序之后访问redis 都通过 127.0.0.1:6380
    listen 6380;
    proxy_pass backend-redis;
  }
}

重启nginx后生效

/usr/local/nginx/sbin/nginx -s quit
/usr/local/nginx/sbin/nginx 

总结

方案优点缺点
单机部署简单、只需一台服务器当该服务器出现故障则无法提供Redis服务
哨兵模式当无法连接的Redis节点小于一半(不包含一半)时任然可以提供服务需要3台及以上服务器
集群模式包含哨兵模式的优点,又具备负载均衡的特性包含哨兵模式的缺点,且不具备在线扩展的能力,部署难度也相对提高
互为主从(keepalived版)2台服务器即可实现,基本可实现非人为干预高可用从服务出现问题到切换完成需要一定时间,可能需要1.2秒。部署需要而外申请一个IP用作VIP
互为主从(上文方式)2台服务器即可实现,基本可实现非人为干预高可用,不用而外申请IPcrontab最短为1分钟执行一次,也就是说从出问题到切换成功可能需要1分钟(当然也可以用脚本循环,不交给定时任务),该服务会被绑定到nginx上一但连接的Nginx出问题,该服务也会无法访问

最合适当下的才是最好的

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值