麒麟v10系统部署 Redis 7.4.7(支持单机/主从/哨兵模式)

麒麟v10系统部署 Redis 7.4.7(支持单机/主从/哨兵模式)

📋 概述

本安装脚本用于在 Kylin Linux Advanced Server V10 (ARM64) 系统上部署 Redis 7.4.7,支持三种部署模式:
单机模式 (Standalone):独立运行的 Redis 实例
主从模式 (Replication):一主一从的高可用架构
哨兵模式 (Sentinel):主从架构 + 自动故障转移

脚本放到了末尾,自行验证。

🖥️ 环境信息

  • 操作系统: Kylin Linux Advanced Server V10 (Halberd)
  • 架构: aarch64 (ARM64)
  • 内核版本: 4.19.90-89.23.v2401.ky10.aarch64
  • Redis 版本: 7.4.7
  • Jemalloc 版本: 5.3.0(源码编译)

🎯 部署模式选择

特性 单机模式 主从模式 哨兵模式
适用场景 开发、测试 生产环境 生产环境(推荐)
服务器数量 1 台 2 台(1 主 1 从) 2 台 Redis(三台也行自行修改SLAVE_IP) + 3 台哨兵
数据冗余 ❌ 无 ✅ 主从同步 ✅ 主从同步
读写分离 ❌ 不支持 ✅ 支持 ✅ 支持
自动故障转移 ❌ 不支持 ❌ 需手动切换 ✅ 自动切换
配置复杂度 简单 中等 较复杂
IP 要求 无限制 需配置主从 IP 需配置主从 + 哨兵 IP

📍 节点信息示例

主从模式

  • 主节点: 10.202.17.12
  • 从节点: 10.202.17.13

哨兵模式

  • 主节点: 10.202.17.12
  • 从节点: 10.202.17.13,10.202.17.14(在SLAVE_IP这里切换ip就行)
  • 哨兵节点: 10.202.17.12, 10.202.17.13, 10.202.17.14(可与 Redis 同机部署)

✨ 功能特性

多种部署模式
– 单机模式:适用于开发、测试或小规模生产环境
– 主从模式:适用于需要高可用的生产环境
– 哨兵模式:适用于需要自动故障转移的生产环境

智能检测
– 自动检测系统环境和架构
– 根据配置自动识别部署模式
– 主从/哨兵模式下自动检测节点角色(主/从/哨兵)
– 检查运行用户是否存在
– 避免重复添加系统配置

源码编译
– jemalloc 5.3.0 源码编译安装
– Redis 使用 jemalloc 内存分配器
ARM64 架构

灵活配置
– 支持单机、主从、哨兵三种部署模式
– 支持完全自定义安装路径
– 通过 config.sh 集中管理配置
– 主从/哨兵模式下自动识别节点角色
– 哨兵可与 Redis 同机部署或独立部署

系统优化
– 内核参数优化
– 禁用透明大页
– 资源限制配置
– Systemd 服务管理

安全加固
– 密码认证
– 用户权限隔离
– 安全的服务停止方式(TERM 信号)

持久化
– RDB 快照
– AOF 日志
– 自动保存策略

📁 文件清单

redis_install/
├── redis-7.4.7.tar.gz           # Redis 源码包(必需)
├── jemalloc-5.3.0.tar.bz2       # jemalloc 源码包(必需)
├── install_redis.sh              # 主安装脚本(必需)
├── config.sh                     # 配置文件(推荐)
├── config.sh.example             # 配置文件示例(参考)
└── README.md                     # 说明文档

redis版本下载地址:https://download.redis.io/releases/

jemalloc-5.3.0下载地址:https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2

🚀 快速开始

1. 前置要求

必须满足的条件
– ✅ Root 权限
– ✅ 运行用户已存在(默认:gzapps)
– ✅ 所有源码包已上传
– ✅ 主从模式下:当前主机 IP 必须为主节点或从节点 IP

创建运行用户(如果不存在):

useradd -r -s /sbin/nologin gzapps

2. 修改配置

方法一:使用示例配置文件(推荐)

# 复制示例配置文件
cp config.sh.example config.sh

# 编辑配置
vim config.sh

方法二:直接编辑现有配置

vim config.sh

重要配置项

2.1 选择部署模式

# ============================================================================
# 部署模式配置
# ============================================================================
# 可选值: standalone (单机) / replication (主从) / sentinel (哨兵)
export DEPLOY_MODE="standalone"

2.2 基础配置(所有模式都需要)

# 安装目录(根据实际情况修改)
export INSTALL_DIR="/AppHome/redis"        # Redis 安装目录
export CONFIG_DIR="/AppHome/redis/conf"    # 配置文件目录
export DATA_DIR="/AppHome/redis/data"      # 数据目录
export LOG_DIR="/AppHome/redis/logs"       # 日志目录

# 运行用户(必须已存在)
export REDIS_USER="gzapps"

# Redis 密码(必须修改)
export REDIS_PASSWORD="1q2w3e4rAa!@#"

# 性能配置
export REDIS_MAXMEMORY="4gb"

2.3 主从模式额外配置(仅在 DEPLOY_MODE=replication 或 sentinel 时需要)

# 主从节点 IP
export MASTER_IP="10.202.17.12"
export SLAVE_IP="10.202.17.13"
#export SLAVE_IP="10.202.17.14"

2.4 哨兵模式额外配置(仅在 DEPLOY_MODE=sentinel 时需要)

# 哨兵配置
export SENTINEL_PORT=26379                                      # 哨兵端口
export SENTINEL_QUORUM=2                                        # 投票数(建议:哨兵总数/2+1)
export SENTINEL_IPS="10.202.17.12 10.202.17.13 10.202.17.14"  # 哨兵节点 IP(空格分隔)
export MASTER_NAME="mymaster"                                   # 主节点名称

哨兵部署说明
– 哨兵可以与 Redis 主从节点同机部署(如示例中的 10.202.17.12 和 10.202.17.13)
– 也可以部署在独立节点上(如示例中的 10.202.17.14)
– 建议至少部署 3 个哨兵节点以保证高可用
– SENTINEL_QUORUM 建议设置为哨兵总数的一半加一

3. 执行安装

3.1 单机模式安装

cd redis_install
chmod +x *.sh

# 确保 config.sh 中 DEPLOY_MODE="standalone"
./install_redis.sh

3.3 哨兵模式安装

在主节点 (10.202.17.12) 上

cd redis_install
chmod +x *.sh

# 确保 config.sh 中 DEPLOY_MODE="sentinel"
# 确保当前 IP 在 SENTINEL_IPS 列表中(如果要部署哨兵)
./install_redis.sh

在从节点 (10.202.17.13) 上

cd redis_install
chmod +x *.sh

# 使用相同的 config.sh 配置
./install_redis.sh

在哨兵节点 (10.202.17.14) 上(如果是独立哨兵节点):

cd redis_install
chmod +x *.sh

# 使用相同的 config.sh 配置
# 注意:独立哨兵节点的 IP 不在 MASTER_IP 和 SLAVE_IP 中
# 但必须在 SENTINEL_IPS 列表中
./install_redis.sh

说明
– 哨兵可以与 Redis 主从节点同机部署
– 脚本会自动判断当前节点是否需要部署哨兵服务
– 建议至少部署 3 个哨兵节点

4. 验证安装

4.1 单机模式验证

# 检查服务状态
systemctl status redis

# 测试连接
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# ping

# 查看 Redis 信息
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info server

4.2 主从模式验证

# 检查服务状态
systemctl status redis

# 测试连接
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# ping

# 查看主从状态
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

4.3 哨兵模式验证

# 检查 Redis 服务状态
systemctl status redis

# 测试 Redis 连接
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# ping

# 查看主从状态
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

# 检查哨兵服务状态(在哨兵节点上)
systemctl status redis-sentinel

# 查看哨兵监控的主节点信息
/AppHome/redis/bin/redis-cli -p 26379 sentinel masters

# 查看哨兵监控的从节点信息
/AppHome/redis/bin/redis-cli -p 26379 sentinel slaves mymaster

# 查看所有哨兵节点
/AppHome/redis/bin/redis-cli -p 26379 sentinel sentinels mymaster

📂 安装后的目录结构

/AppHome/redis/
├── bin/                          # 可执行文件
│   ├── redis-server              # Redis 服务器
│   ├── redis-cli                 # Redis 客户端
│   ├── redis-benchmark           # 性能测试工具
│   ├── redis-check-rdb           # RDB 检查工具
│   └── redis-check-aof           # AOF 检查工具
├── conf/
│   └── redis.conf                # Redis 配置文件
├── data/
│   ├── dump.rdb                  # RDB 持久化文件
aof            # AOF 持久化文件
│   └── redis_6379.pid            # PID 文件
└── logs/
    └── redis.log                 # Redis 日志文件

/usr/local/lib/
└── libjemalloc.so.*              # jemalloc 库文件

/etc/systemd/system/
└── redis.service                 # Systemd 服务文件

⚙️ 配置说明

Redis 核心配置

配置项 说明
bind 0.0.0.0 监听所有网卡
port 6379 监听端口
daemonize yes 后台运行
requirepass 自定义 Redis 密码
maxmemory 4gb 最大内存限制
maxmemory-policy allkeys-lru LRU 淘汰策略
hz 100 高频率定时任务
appendonly yes 启用 AOF
save 900 1 / 300 10 / 60 10000 RDB 保存策略

系统优化参数

脚本会自动配置(避免重复添加):

# 内核参数
vm.overcommit_memory = 1          # 允许内存过量分配
net.core.somaxconn = 65535        # 增加连接队列

# 禁用透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 资源限制
gzapps soft nofile 65535
gzapps hard nofile 65535
gzapps soft nproc 65535
gzapps hard nproc 65535

Systemd 服务配置

[Service]
Type=forking                      # 后台进程模式
User=gzapps                       # 运行用户
PIDFile=/AppHome/redis/data/redis_6379.pid
ExecStart=/AppHome/redis/bin/redis-server /AppHome/redis/conf/redis.conf
ExecStop=/bin/kill -s TERM $MAINPID  # 优雅停止
TimeoutStartSec=30s               # 启动超时
Restart=always                    # 自动重启
LimitNOFILE=65535                 # 文件描述符限制

🔧 服务管理

基本操作

# 启动服务
systemctl start redis

# 停止服务
systemctl stop redis

# 重启服务
systemctl restart redis

# 查看状态
systemctl status redis

# 查看日志
journalctl -u redis -f

# 开机自启
systemctl enable redis

# 禁用自启
systemctl disable redis

Redis 命令

# 连接 Redis
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@#

# 查看信息
redis-cli -a 1q2w3e4rAa!@# info
redis-cli -a 1q2w3e4rAa!@# info replication
redis-cli -a 1q2w3e4rAa!@# info memory

# 查看配置
redis-cli -a 1q2w3e4rAa!@# config get "*"

# 查看慢查询
redis-cli -a 1q2w3e4rAa!@# slowlog get 10

# 查看客户端连接
redis-cli -a 1q2w3e4rAa!@# client list

🔍 主从复制验证(仅主从模式)

在主节点验证

/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

期望输出

Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
# Replication
role:master
connected_slaves:1
slave0:ip=10.202.17.13,port=6379,state=online,offset=1632,lag=0
master_failover_state:no-failover
master_replid:f6f7770caf8dc4d40f47a30824ec181caa3c6676
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1632
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:1631

在从节点验证

/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

期望输出

Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
# Replication
role:slave
master_host:10.202.17.12
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_read_repl_offset:1660
slave_repl_offset:1660
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f6f7770caf8dc4d40f47a30824ec181caa3c6676
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1660
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:804
repl_backlog_histlen:857

测试数据同步

主节点写入

redis-cli -a 1q2w3e4rAa!@# set test_key "hello from master"

从节点读取

redis-cli -portRedis@2026 get test_key
# 输出: "hello from master"

🐛 故障排查

1. 服务启动超时

症状systemctl start redis 超时

排查步骤

# 1. 查看 systemd 日志
journalctl -u redis -n 50

# 2. 查看 Redis 日志
tail -100 /AppHome/redis/logs/redis.log

# 3. 检查 PID 文件
ls -la /AppHome/redis/data/redis_6379.pid

# 4. 手动启动测试
/AppHome/redis/bin/redis-server /AppHome/redis/conf/redis.conf

2. 主从同步失败

症状:从节点 master_link_status:down

排查步骤

# 1. 检查网络连通性
ping 10.202.17.12
telnet 10.202.17.12 6379

# 2. 检查防火墙
firewall-cmd --list-all

# 3. 检查主节点日志
tail -f /AppHome/redis/logs/redis.log

# 4. 检查密码配置
grep "requirepass\|masterauth" /AppHome/redis/conf/redis.conf

解决方案

# 开放防火墙端口
fird-port=6379/tcp
firewall-cmd --reload

redis-server /AppHome/redis/conf/redis.conf --test-memory 1
   ```

3. **检查资源**
   ```bash
   top
   free -h
   df -h
   ```

4. **检查网络**
   ```bash
   netstat -tlnp | grep 6379
   ss -tlnp | grep 6379
   ```

---


## 🔄 哨兵模式故障转移测试(仅哨兵模式)

### 测试自动故障转移

**1. 模拟主节点故障**

在主节点上停止 Redis 服务:
```bash
systemctl stop redis

2. 观察哨兵日志

在任意哨兵节点上查看日志:

tail -f /AppHome/redis/logs/sentinel.log

你会看到类似以下日志:

+sdown master mymaster 10.202.17.12 6379
+odown master mymaster 10.202.17.12 6379 #quorum 2/2
+failover-triggered master mymaster 10.202.17.12 6379
+failover-state-select-slave master mymaster 10.202.17.12 6379
+selected-slave slave 10.202.17.13:6379 10.202.17.13 6379 @ mymaster 10.202.17.12 6379
+failover-state-send-slaveof-noone slave 10.202.17.13:6379 10.202.17.13 6379 @ mymaster 10.202.17.12 6379
+failover-end master mymaster 10.202.17.12 6379
+switch-master mymaster 10.202.17.12 6379 10.202.17.13 6379

3. 验证新主节点

查看哨兵监控的主节点信息:

/AppHome/redis/bin/redis-cli -p 26379 sentinel get-master-addr-by-name mymaster

应该返回新的主节点 IP(原从节点):

1) "10.202.17.13"
2) "6379"

4. 恢复原主节点

启动原主节点的 Redis 服务:

systemctl start redis

原主节点会自动变成从节点,同步新主节点的数据。

验证故障转移结果

# 在新主节点上查看角色
/AppHome/redis/bin/redis-cli -h 10.202.17.13 -a 1q2w3e4rAa!@# info replication | grep role
# 输出: role:master

# 在原主节点上查看角色
/AppHome/redis/bin/redis-cli -h 10.202.17.12 -a 1q2w3e4rAa!@# info replication | grep role
# 输出: role:slave

💾 备份与恢复

自动备份脚本

创建 /usr/local/bin/redis_backup.sh

#!/bin/bash
BACKUP_DIR="/backup/redis/$(date +%Y%m%d)"
mkdir -p $BACKUP_DIR

# 触发 RDB 快照
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# bgsave

# 等待快照完成
sleep 10

# 备份文件
cp /AppHome/redis/data/dump.rdb $BACKUP_DIR/
cp /AppHome/redis/data/appendonly.aof $BACKUP_DIR/
cp /AppHome/redis/conf/redis.conf $BACKUP_DIR/

# 保留最近 7 天
find /backup/redis/ -type d -mtime +7 -exec rm -rf {} \;

添加到 crontab

# 每天凌晨 2 点备份
0 2 * * * /usr/local/bin/redis_backup.sh

📊 性能测试

基础性能测试

/AppHome/redis/bin/redis-benchmark \
  -h 127.0.0.1 \
  -p 6379 \
  -a 1q2w3e4rAa!@# \
  -c 50 \
  -n 10000

内存不足

症状:Redis 拒绝写入

排查步骤

# 查看内存使用
redis-cli -a 1q2w3e4rAa!@# info memory | grep used_memory_human
redis-cli -a 1q2w3e4rAa!@# info memory | grep maxmemory_human

解决方案

# 临时调整(重启后失效)
redis-cli -a 1q2w3e4rAa!@# config set maxmemory 8gb

脚本如下

config.sh

#!/bin/bash

################################################################################
# Redis 安装配置文件
# 
# 使用方法:
# 1. 修改下面的配置参数
# 2. 将此文件和 install_redis.sh 放在同一目录
# 3. 运行 install_redis.sh 时会自动加载此配置
################################################################################

# ============================================================================
# 部署模式配置
# ============================================================================
# 可选值: 
#   - standalone   : 单机模式
#   - replication  : 主从模式
#   - sentinel     : 主从+哨兵模式
export DEPLOY_MODE="standalone"

# ============================================================================
# Redis 版本配置
# ============================================================================
export REDIS_VERSION="7.4.7"

# ============================================================================
# 安装目录配置
# ============================================================================
export INSTALL_DIR="/AppHome/redis"        # Redis 安装目录
export CONFIG_DIR="/AppHome/redis/conf"    # 配置文件目录(redis.conf 存放位置)
export DATA_DIR="/AppHome/redis/data"      # 数据存储目录
export LOG_DIR="/AppHome/redis/logs"       # 日志目录

# ============================================================================
# Redis 服务配置
# ============================================================================
export REDIS_PORT=6379                     # Redis 监听端口
export REDIS_USER="gzapps"                 # Redis 运行用户
export REDIS_PASSWORD="YourStrongPassword123"  # Redis 密码

# ============================================================================
# 主从节点 IP 配置(仅在 DEPLOY_MODE=replication 或 sentinel 时生效)
# ============================================================================
export MASTER_IP="10.202.17.12"            # 主节点 IP
export SLAVE_IP="10.202.17.13"             # 从节点 IP

# ============================================================================
# 哨兵配置(仅在 DEPLOY_MODE=sentinel 时生效)
# ============================================================================
export SENTINEL_PORT=26379                 # 哨兵监听端口
export SENTINEL_QUORUM=2                   # 哨兵判定主节点下线所需的投票数
export SENTINEL_IPS="10.202.17.12 10.202.17.13 10.202.17.14"  # 哨兵节点 IP 列表(空格分隔)
export MASTER_NAME="mymaster"              # 主节点名称(哨兵监控用)

# ============================================================================
# 性能配置
# ============================================================================
export REDIS_MAXMEMORY="4gb"               # 最大内存限制(根据服务器实际内存调整)

# ============================================================================
# 持久化配置
# ============================================================================
export ENABLE_AOF="yes"                    # 是否启用 AOF 持久化

install_redis.sh

#!/bin/bash

################################################################################
# Redis 7.4.7 安装脚本(支持单机/主从/哨兵模式)
# 适用系统: Kylin Linux Advanced Server V10 (ARM64)
# 作者: glj
# 日期: 2025-02-10
# 
# 部署模式:
#   - standalone: 单机模式
#   - replication: 主从模式
#   - sentinel: 主从+哨兵模式
################################################################################

set -e  # 遇到错误立即退出

# ============================================================================
# 加载外部配置文件(如果存在)
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/config.sh" ]]; then
    source "$SCRIPT_DIR/config.sh"
    echo "[INFO] 已加载外部配置文件: config.sh"
fi

# ============================================================================
# 全局变量配置(可被 config.sh 覆盖)
# ============================================================================
DEPLOY_MODE="${DEPLOY_MODE:-standalone}"         # 部署模式:standalone、replication 或 sentinel
REDIS_VERSION="${REDIS_VERSION:-7.4.7}"
REDIS_TAR="redis-${REDIS_VERSION}.tar.gz"
REDIS_DIR="redis-${REDIS_VERSION}"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/redis}"
CONFIG_DIR="${CONFIG_DIR:-/etc/redis}"           # 配置文件目录(可自定义)
DATA_DIR="${DATA_DIR:-/data/redis}"
LOG_DIR="${LOG_DIR:-/var/log/redis}"
REDIS_PORT="${REDIS_PORT:-6379}"
REDIS_USER="${REDIS_USER:-redis}"
REDIS_PASSWORD="${REDIS_PASSWORD:-YourStrongPassword123}"  # 请修改为实际密码
MAXMEMORY="${REDIS_MAXMEMORY:-2gb}"

# 主从配置(仅在 replication 或 sentinel 模式下使用)
MASTER_IP="${MASTER_IP:-10.202.17.12}"
SLAVE_IP="${SLAVE_IP:-10.202.17.13}"

# 哨兵配置(仅在 sentinel 模式下使用)
SENTINEL_PORT="${SENTINEL_PORT:-26379}"
SENTINEL_QUORUM="${SENTINEL_QUORUM:-2}"
SENTINEL_IPS="${SENTINEL_IPS:-10.202.17.12 10.202.17.13 10.202.17.14}"
MASTER_NAME="${MASTER_NAME:-mymaster}"

# 节点角色和当前 IP(由 check_node_role 函数设置)
CURRENT_IP=""
NODE_ROLE=""
IS_SENTINEL_NODE=false  # 当前节点是否为哨兵节点

# 配置文件完整路径
REDIS_CONF="${CONFIG_DIR}/redis.conf"
SENTINEL_CONF="${CONFIG_DIR}/sentinel.conf"

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# ============================================================================
# 工具函数
# ============================================================================

# 日志输出函数
log_info() {
    echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

# 检查命令是否存在
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# 检查是否为 root 用户
check_root() {
    if [[ $EUID -ne 0 ]]; then
        log_error "此脚本必须以 root 用户运行"
        exit 1
    fi
}

# 检查当前主机角色(根据部署模式)
check_node_role() {
    log_info "检查部署模式: $DEPLOY_MODE"

    # 验证部署模式配置
    if [[ "$DEPLOY_MODE" != "standalone" && "$DEPLOY_MODE" != "replication" && "$DEPLOY_MODE" != "sentinel" ]]; then
        log_error "无效的部署模式: $DEPLOY_MODE"
        log_error "有效值: standalone (单机) / replication (主从) / sentinel (哨兵)"
        exit 1
    fi

    # 获取当前主机 IP
    CURRENT_IP=$(hostname -I | awk '{print $1}')
    log_info "当前主机 IP: $CURRENT_IP"

    # 单机模式:直接设置为 standalone
    if [[ "$DEPLOY_MODE" == "standalone" ]]; then
        NODE_ROLE="standalone"
        log_info "部署模式: 单机模式 (Standalone)"
        return 0
    fi

    # 主从模式或哨兵模式:检查当前主机角色
    if [[ "$DEPLOY_MODE" == "replication" || "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "部署模式: ${DEPLOY_MODE} 模式"
        log_info "检查当前主机角色..."

        if [[ "$CURRENT_IP" == "$MASTER_IP" ]]; then
            NODE_ROLE="master"
            log_info "当前节点为主节点 (Master)"
        elif [[ "$CURRENT_IP" == "$SLAVE_IP" ]]; then
            NODE_ROLE="slave"
            log_info "当前节点为从节点 (Slave)"
        else
            log_error "当前 IP ($CURRENT_IP) 不在配置的主从节点中"
            log_error "配置的主节点: $MASTER_IP"
            log_error "配置的从节点: $SLAVE_IP"
            log_error "请检查 config.sh 中的 MASTER_IP 和 SLAVE_IP 配置"
            exit 1
        fi
    fi

    # 哨兵模式:额外检查是否为哨兵节点
    if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "检查是否为哨兵节点..."

        # 将哨兵 IP 列表转换为数组
        local sentinel_ips_array=($SENTINEL_IPS)

        # 检查当前 IP 是否在哨兵列表中
        for sentinel_ip in "${sentinel_ips_array[@]}"; do
            if [[ "$CURRENT_IP" == "$sentinel_ip" ]]; then
                IS_SENTINEL_NODE=true
                log_info "当前节点同时为哨兵节点 (Sentinel)"
                break
            fi
        done

        if [[ "$IS_SENTINEL_NODE" == false ]]; then
            log_warn "当前节点不在哨兵列表中,将只部署 Redis 服务"
        fi
    fi
}

# 检查系统信息
check_system() {
    log_info "检查系统信息..."

    # 检查操作系统
    if [[ -f /etc/os-release ]]; then
        . /etc/os-release
        log_info "操作系统: $PRETTY_NAME"
    fi

    # 检查架构
    ARCH=$(uname -m)
    log_info "系统架构: $ARCH"

    # 检查内核版本
    KERNEL=$(uname -r)
    log_info "内核版本: $KERNEL"
}

# ============================================================================
# 依赖检查与安装
# ============================================================================

# 检查并安装编译依赖
install_dependencies() {
    log_info "检查并安装编译依赖..."

    local packages=(
        "gcc"
        "make"
        "tcl"
        "wget"
        "tar"
        "bzip2"
    )

    local to_install=()

    # 检查哪些包需要安装
    for pkg in "${packages[@]}"; do
        if ! rpm -q "$pkg" &>/dev/null; then
            to_install+=("$pkg")
        fi
    done

    # 如果有需要安装的包
    if [[ ${#to_install[@]} -gt 0 ]]; then
        log_info "需要安装的包: ${to_install[*]}"
        log_info "正在安装依赖包(请稍候...)"
        yum install -y "${to_install[@]}" > /dev/null 2>&1
        if [[ $? -eq 0 ]]; then
            log_info "依赖包安装完成"
        else
            log_warn "部分依赖包安装失败,继续执行..."
        fi
    else
        log_info "所有依赖包已安装"
    fi
}

# 编译安装 jemalloc
install_jemalloc() {
    log_info "开始编译安装 jemalloc..."

    local JEMALLOC_VERSION="5.3.0"
    local JEMALLOC_TAR="jemalloc-${JEMALLOC_VERSION}.tar.bz2"
    local JEMALLOC_DIR="jemalloc-${JEMALLOC_VERSION}"
    local BUILD_LOG="/tmp/jemalloc_build.log"

    # 检查是否已安装
    if ldconfig -p | grep -q libjemalloc; then
        log_info "jemalloc 已安装,跳过编译"
        return 0
    fi

    # 检查源码包是否存在
    if [[ ! -f "$JEMALLOC_TAR" ]]; then
        log_error "jemalloc 源码包 $JEMALLOC_TAR 不存在"
        log_error "请将 jemalloc-5.3.0.tar.bz2 放到当前目录"
        return 1
    fi

    log_info "使用本地 jemalloc 源码包: $JEMALLOC_TAR"

    # 解压
    log_info "解压 jemalloc..."
    tar -xjf "$JEMALLOC_TAR" 2>&1 | tee "$BUILD_LOG" > /dev/null
    cd "$JEMALLOC_DIR"

    # 生成配置文件
    log_info "生成配置文件..."
    ./autogen.sh >> "$BUILD_LOG" 2>&1 || {
        log_warn "autogen.sh 失败,尝试直接 configure"
    }

    # 配置
    log_info "配置 jemalloc..."
    ./configure --prefix=/usr/local >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "配置失败,查看日志: $BUILD_LOG"
        cd ..
        return 1
    fi

    # 编译
    log_info "编译 jemalloc(使用 $(nproc) 核心,请稍候...)"
    make -j$(nproc) >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "编译失败,查看日志: $BUILD_LOG"
        cd ..
        return 1
    fi

    # 安装
    log_info "安装 jemalloc..."
    make install >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "安装失败,查看日志: $BUILD_LOG"
        cd ..
        return 1
    fi

    # 配置库路径
    log_info "配置库路径..."
    echo "/usr/local/lib" > /etc/ld.so.conf.d/jemalloc.conf
    ldconfig

    # 返回上级目录
    cd ..

    # 验证安装
    if ldconfig -p | grep -q libjemalloc; then
        log_info "jemalloc 安装成功"
        # 清理临时文件
        rm -rf "$JEMALLOC_DIR"
        rm -f "$BUILD_LOG"
        return 0
    else
        log_error "jemalloc 安装失败,查看日志: $BUILD_LOG"
        return 1
    fi
}

# ============================================================================
# Redis 编译安装
# ============================================================================

# 编译 Redis
compile_redis() {
    log_info "开始编译 Redis ${REDIS_VERSION}..."

    local BUILD_LOG="/tmp/redis_build.log"

    # 检查源码包是否存在
    if [[ ! -f "$REDIS_TAR" ]]; then
        log_error "Redis 源码包 $REDIS_TAR 不存在"
        exit 1
    fi

    # 解压源码
    log_info "解压 Redis 源码..."
    tar -zxf "$REDIS_TAR" 2>&1 | tee "$BUILD_LOG" > /dev/null
    cd "$REDIS_DIR"

    # 使用 jemalloc 编译(Redis 官方推荐方式)
    log_info "使用 jemalloc 编译 Redis(使用 $(nproc) 核心,请稍候...)"
    make MALLOC=jemalloc -j$(nproc) >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "编译失败,查看日志: $BUILD_LOG"
        cd ..
        exit 1
    fi

    # 安装
    log_info "安装 Redis 到 $INSTALL_DIR..."
    make PREFIX="$INSTALL_DIR" install >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "安装失败,查看日志: $BUILD_LOG"
        cd ..
        exit 1
    fi

    cd ..
    log_info "Redis 编译安装完成"
    rm -f "$BUILD_LOG"
}

# ============================================================================
# 系统配置
# ============================================================================

# 检查或创建 Redis 用户
check_redis_user() {
    log_info "检查 Redis 用户: $REDIS_USER"

    if ! id "$REDIS_USER" &>/dev/null; then
        log_error "用户 $REDIS_USER 不存在"
        log_error "请先创建用户,或修改 config.sh 中的 REDIS_USER 配置"
        log_error "创建用户命令: useradd $REDIS_USER"
        exit 1
    else
        log_info "用户 $REDIS_USER 已存在"
    fi
}

# 创建目录结构
create_directories() {
    log_info "创建目录结构..."

    # 创建所有必要的目录
    mkdir -p "$INSTALL_DIR"
    mkdir -p "$DATA_DIR"
    mkdir -p "$LOG_DIR"
    mkdir -p "$CONFIG_DIR"

    # 统一设置所有目录的所有者为 Redis 用户
    log_info "设置目录权限..."
    chown -R "$REDIS_USER:$REDIS_USER" "$INSTALL_DIR"
    chown -R "$REDIS_USER:$REDIS_USER" "$DATA_DIR"
    chown -R "$REDIS_USER:$REDIS_USER" "$LOG_DIR"
    chown -R "$REDIS_USER:$REDIS_USER" "$CONFIG_DIR"

    log_info "目录创建完成"
}

# 配置系统参数
configure_system() {
    log_info "配置系统参数..."

    # 配置内核参数(避免重复添加)
    if ! grep -q "vm.overcommit_memory" /etc/sysctl.conf; then
        log_info "添加内核参数到 /etc/sysctl.conf..."
        cat >> /etc/sysctl.conf <<EOF

# Redis 优化参数
vm.overcommit_memory = 1
net.core.somaxconn = 65535
EOF
        sysctl -p
    else
        log_info "内核参数已存在,跳过添加"
    fi

    # 禁用透明大页
    if [[ -f /sys/kernel/mm/transparent_hugepage/enabled ]]; then
        echo never > /sys/kernel/mm/transparent_hugepage/enabled
        echo never > /sys/kernel/mm/transparent_hugepage/defrag
        log_info "已禁用透明大页"
    fi

    # 配置 limits(避免重复添加)
    if ! grep -q "^$REDIS_USER.*nofile" /etc/security/limits.conf; then
        log_info "添加资源限制到 /etc/security/limits.conf..."
        cat >> /etc/security/limits.conf <<EOF

# Redis 资源限制
$REDIS_USER soft nofile 65535
$REDIS_USER hard nofile 65535
$REDIS_USER soft nproc 65535
$REDIS_USER hard nproc 65535
EOF
    else
        log_info "资源限制已存在,跳过添加"
    fi

    log_info "系统参数配置完成"
}

# ============================================================================
# Redis 配置
# ============================================================================

# 生成 Redis 配置文件
generate_redis_config() {
    log_info "生成 Redis 配置文件..."

    # 使用之前检查的节点角色
    local role="$NODE_ROLE"
    local mode_desc=""

    # 根据角色设置描述
    case "$role" in
        standalone)
            mode_desc="单机模式 (Standalone)"
            ;;
        master)
            if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
                mode_desc="哨兵模式 - 主节点 (Sentinel Master)"
            else
                mode_desc="主从模式 - 主节点 (Master)"
            fi
            ;;
        slave)
            if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
                mode_desc="哨兵模式 - 从节点 (Sentinel Slave)"
            else
                mode_desc="主从模式 - 从节点 (Slave)"
            fi
            ;;
    esac

    log_info "生成配置: ${mode_desc}"

    # 生成配置文件
    cat > "$REDIS_CONF" <<EOF
# Redis ${REDIS_VERSION} 配置文件
# 部署模式: ${mode_desc}
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')

# ============================================================================
# 网络配置
# ============================================================================
bind 0.0.0.0
protected-mode yes
port ${REDIS_PORT}
tcp-backlog 511
timeout 0
tcp-keepalive 300

# ============================================================================
# 通用配置
# ============================================================================
daemonize yes
pidfile ${DATA_DIR}/redis_${REDIS_PORT}.pid
loglevel notice
logfile ${LOG_DIR}/redis.log
databases 16
hz 100

# ============================================================================
# 持久化配置
# ============================================================================
# RDB 配置
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ${DATA_DIR}

# AOF 配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes

# ============================================================================
# 安全配置
# ============================================================================
requirepass ${REDIS_PASSWORD}

# ============================================================================
# 内存管理
# ============================================================================
maxmemory ${MAXMEMORY}
maxmemory-policy allkeys-lru
maxmemory-samples 5

# ============================================================================
# 慢查询日志
# ============================================================================
slowlog-log-slower-than 10000
slowlog-max-len 128

# ============================================================================
# 客户端配置
# ============================================================================
maxclients 10000

EOF

    # 如果是从节点,添加主从复制配置
    if [[ "$role" == "slave" ]]; then
        log_info "添加主从复制配置..."
        cat >> "$REDIS_CONF" <<EOF
# ============================================================================
# 主从复制配置
# ============================================================================
replicaof ${MASTER_IP} ${REDIS_PORT}
masterauth ${REDIS_PASSWORD}
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100

EOF
    fi

    # 设置配置文件权限
    chown "$REDIS_USER:$REDIS_USER" "$REDIS_CONF"
    chmod 640 "$REDIS_CONF"

    log_info "Redis 配置文件生成完成: $REDIS_CONF"
}

# 生成哨兵配置文件
generate_sentinel_config() {
    log_info "生成 Sentinel 配置文件..."

    # 生成哨兵配置
    cat > "$SENTINEL_CONF" <<EOF
# Redis Sentinel ${REDIS_VERSION} 配置文件
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')

# ============================================================================
# 网络配置
# ============================================================================
bind 0.0.0.0
port ${SENTINEL_PORT}
daemonize yes
pidfile ${DATA_DIR}/redis-sentinel.pid
logfile ${LOG_DIR}/sentinel.log

# ============================================================================
# 监控配置
# ============================================================================
# 监控主节点:sentinel monitor <master-name> <ip> <port> <quorum>
# quorum: 判定主节点下线所需的哨兵投票数
sentinel monitor ${MASTER_NAME} ${MASTER_IP} ${REDIS_PORT} ${SENTINEL_QUORUM}

# 主节点密码
sentinel auth-pass ${MASTER_NAME} ${REDIS_PASSWORD}

# ============================================================================
# 故障转移配置
# ============================================================================
# 主节点无响应超时时间(毫秒)
sentinel down-after-milliseconds ${MASTER_NAME} 5000

# 故障转移超时时间(毫秒)
sentinel failover-timeout ${MASTER_NAME} 15000

# 同时进行同步的从节点数量
sentinel parallel-syncs ${MASTER_NAME} 1

EOF

    # 设置配置文件权限
    chown "$REDIS_USER:$REDIS_USER" "$SENTINEL_CONF"
    chmod 640 "$SENTINEL_CONF"

    log_info "Sentinel 配置文件生成完成: $SENTINEL_CONF"
}

# ============================================================================
# Systemd 服务配置
# ============================================================================

# 创建 systemd 服务文件
create_systemd_service() {
    log_info "创建 systemd 服务..."

    # 创建 Redis 服务
    cat > /etc/systemd/system/redis.service <<EOF
[Unit]
Description=Redis In-Memory Data Store
After=network.target

[Service]
Type=forking
User=${REDIS_USER}
Group=${REDIS_USER}
PIDFile=${DATA_DIR}/redis_${REDIS_PORT}.pid
ExecStart=${INSTALL_DIR}/bin/redis-server ${REDIS_CONF}
ExecStop=/bin/kill -s TERM \$MAINPID
TimeoutStartSec=30s
TimeoutStopSec=30s
Restart=always
RestartSec=5s
LimitNOFILE=65535

# 安全加固
PrivateTmp=yes
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF

    log_info "Redis 服务创建完成"

    # 如果是哨兵模式且当前节点是哨兵节点,创建哨兵服务
    if [[ "$DEPLOY_MODE" == "sentinel" && "$IS_SENTINEL_NODE" == true ]]; then
        log_info "创建 Sentinel 服务..."

        cat > /etc/systemd/system/redis-sentinel.service <<EOF
[Unit]
Description=Redis Sentinel
After=network.target redis.service
Requires=redis.service

[Service]
Type=forking
User=${REDIS_USER}
Group=${REDIS_USER}
PIDFile=${DATA_DIR}/redis-sentinel.pid
ExecStart=${INSTALL_DIR}/bin/redis-sentinel ${SENTINEL_CONF}
ExecStop=/bin/kill -s TERM \$MAINPID
TimeoutStartSec=30s
TimeoutStopSec=30s
Restart=always
RestartSec=5s
LimitNOFILE=65535

# 安全加固
PrivateTmp=yes
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF

        log_info "Sentinel 服务创建完成"
    fi

    # 重载 systemd
    systemctl daemon-reload

    log_info "Systemd 服务配置完成"
}

# ============================================================================
# 启动与验证
# ============================================================================

# 启动 Redis 服务
start_redis() {
    log_info "启动 Redis 服务..."

    systemctl enable redis
    systemctl start redis

    # 等待服务启动
    sleep 3

    # 检查服务状态
    if systemctl is-active --quiet redis; then
        log_info "Redis 服务启动成功"
    else
        log_error "Redis 服务启动失败"
        systemctl status redis
        exit 1
    fi

    # 如果是哨兵模式且当前节点是哨兵节点,启动哨兵服务
    if [[ "$DEPLOY_MODE" == "sentinel" && "$IS_SENTINEL_NODE" == true ]]; then
        log_info "启动 Sentinel 服务..."

        systemctl enable redis-sentinel
        systemctl start redis-sentinel

        # 等待服务启动
        sleep 3

        # 检查服务状态
        if systemctl is-active --quiet redis-sentinel; then
            log_info "Sentinel 服务启动成功"
        else
            log_error "Sentinel 服务启动失败"
            systemctl status redis-sentinel
            exit 1
        fi
    fi
}

# 验证 Redis 安装
verify_redis() {
    log_info "验证 Redis 安装..."

    # 检查 Redis 版本
    local version=$("$INSTALL_DIR/bin/redis-server" --version | awk '{print $3}' | cut -d'=' -f2)
    log_info "Redis 版本: $version"

    # 检查 jemalloc(通过 Redis INFO 命令)
    local allocator=$("$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning \
        INFO memory 2>/dev/null | grep -E 'mem_allocator' | awk -F: '{print $2}' | tr -d '\r')

    if [[ "$allocator" =~ jemalloc ]]; then
        log_info "Redis 正在使用 jemalloc 内存分配器: $allocator"
    else
        log_warn "Redis 未使用 jemalloc,当前内存分配器: ${allocator:-未知}"
    fi

    # 测试连接(隐藏密码警告)
    log_info "测试 Redis 连接..."
    if "$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning ping 2>/dev/null | grep -q PONG; then
        log_info "Redis 连接测试成功"
    else
        log_error "Redis 连接测试失败"
        exit 1
    fi

    # 根据部署模式检查状态
    if [[ "$DEPLOY_MODE" == "replication" ]]; then
        log_info "检查主从复制状态..."
        "$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning info replication 2>/dev/null
    elif [[ "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "检查主从复制状态..."
        "$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning info replication 2>/dev/null

        # 如果当前节点是哨兵节点,检查哨兵状态
        if [[ "$IS_SENTINEL_NODE" == true ]]; then
            log_info "检查 Sentinel 状态..."
            "$INSTALL_DIR/bin/redis-cli" -p "$SENTINEL_PORT" --no-auth-warning sentinel masters 2>/dev/null
        fi
    else
        log_info "单机模式部署,跳过主从状态检查"
    fi
}

# ============================================================================
# 清理函数
# ============================================================================

cleanup() {
    log_info "清理临时文件..."

    # 清理 Redis 临时目录
    if [[ -d "$REDIS_DIR" ]]; then
        rm -rf "$REDIS_DIR"
        log_info "已清理 Redis 临时文件"
    fi

    # 清理 jemalloc 临时目录
    if [[ -d "jemalloc-5.3.0" ]]; then
        rm -rf "jemalloc-5.3.0"
        log_info "已清理 jemalloc 临时文件"
    fi
}

# ============================================================================
# 主函数
# ============================================================================

main() {
    log_info "=========================================="
    log_info "Redis ${REDIS_VERSION} 安装脚本"
    log_info "=========================================="

    # 前置检查
    check_root
    check_node_role      # 检查部署模式和节点角色
    check_redis_user     # 检查 Redis 用户是否存在
    check_system

    # 安装依赖
    install_dependencies

    # 编译安装 jemalloc
    install_jemalloc

    # 编译安装 Redis
    compile_redis

    # 系统配置
    create_directories
    configure_system

    # Redis 配置
    generate_redis_config

    # 哨兵模式:如果当前节点是哨兵节点,生成哨兵配置
    if [[ "$DEPLOY_MODE" == "sentinel" && "$IS_SENTINEL_NODE" == true ]]; then
        generate_sentinel_config
    fi

    create_systemd_service

    # 启动服务
    start_redis

    # 验证安装
    verify_redis

    # 清理
    # cleanup

    # 输出安装信息
    log_info "=========================================="
    log_info "Redis 安装完成!"
    log_info "=========================================="
    log_info "部署模式: $DEPLOY_MODE"
    log_info "节点角色: $NODE_ROLE"
    log_info "安装目录: $INSTALL_DIR"
    log_info "配置文件: $REDIS_CONF"
    log_info "数据目录: $DATA_DIR"
    log_info "日志目录: $LOG_DIR"
    log_info "服务管理: systemctl {start|stop|restart|status} redis"
    log_info "连接命令: $INSTALL_DIR/bin/redis-cli -a $REDIS_PASSWORD"

    # 主从模式额外提示
    if [[ "$DEPLOY_MODE" == "replication" ]]; then
        log_info "----------------------------------------"
        log_info "主从复制信息:"
        log_info "  主节点: $MASTER_IP"
        log_info "  从节点: $SLAVE_IP"
        log_info "  查看复制状态: $INSTALL_DIR/bin/redis-cli -a $REDIS_PASSWORD info replication"
    fi

    # 哨兵模式额外提示
    if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "----------------------------------------"
        log_info "哨兵模式信息:"
        log_info "  主节点: $MASTER_IP"
        log_info "  从节点: $SLAVE_IP"
        log_info "  哨兵节点: $SENTINEL_IPS"
        log_info "  哨兵端口: $SENTINEL_PORT"
        log_info "  主节点名称: $MASTER_NAME"
        log_info "  查看复制状态: $INSTALL_DIR/bin/redis-cli -a $REDIS_PASSWORD info replication"

        if [[ "$IS_SENTINEL_NODE" == true ]]; then
            log_info "  哨兵配置: $SENTINEL_CONF"
            log_info "  哨兵服务: systemctl {start|stop|restart|status} redis-sentinel"
            log_info "  查看哨兵状态: $INSTALL_DIR/bin/redis-cli -p $SENTINEL_PORT sentinel masters"
        fi
    fi

    log_info "=========================================="
}

# 执行主函数
main "$@"

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇