Redis 学习笔记

2019-09-30  本文已影响0人  Whyn

简介

Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, Streams, HyperLogLogs, Bitmaps.

Redis 通常被用作为数据结构服务器,我们可以通过 Redis 客户端(redis-cli)使用一些命令操作 Redis 服务器(redis-server),redis-cliredis-server 之间通过 Socket 进行通信,因此不同的进程都可以使用同样的方式对同一个数据进行查询和修改。

Redis 最大的特点是:基于内存操作的 NoSql 数据库,Redis 默认将数据存储在内存中,因此具备高效的数据操作性能。但同时,Redis 也具备持久化功能,可以将内存中的数据持久化到磁盘,下次启动时,可以再次加载并进行使用。

Redis 的数据存储模型为 键值对,但其提供了较丰富的值存储类型:字符串(String),列表(List),哈希(Hash),集合(Set),有序集合(zset),HyperLogLogs,Bitmaps···

Redis 实现的数据结构有一些特殊的属性:

优缺点

Redis 优点:

Redis 缺点:

安装

Redis 官方只提供 Linux 版本,因此,对于Windows 版本,需要采用其他方法进行安装。

  1. win10 系统开启虚拟化:任务管理器(ctrl+alt+delete)- Performance - 查看 Virtualization。如果虚拟化未启用(disable),那么需要重启电脑进入 bios 开启虚拟化。
Virtualization
  1. 启用 Windows 系统自带的虚拟机平台 Hyper-v:控制面板 - Programs - Programs and Features - Turn Windows features on or off - 勾选上 Hyper-V - 重启系统
Hyper-v
  1. 下载安装 docker,安装完成后最好更改下镜像源:docker Settings - deamon - Register mirrors,填入https://registry.docker-cn.com
    :控制台输入docker --version,查看 docker 是否安装成功。
    控制台输入docker info,查看镜像源是否配置成功。
docker info
  1. 启动 docker,进行 Redis 镜像安装:docker pull redis

  2. 如果出现以下错误:

no matching manifest for windows/amd64 10.0.xxxxx in the manifest list entries

则需要将 docker 切换到 Linux 容器:docker - Switch to Linux Container...。因为很多软件都是开发给 Linux,不适用于 Windows 架构。
:切换容器后,还需要重新设置下镜像源,方法参照步骤3。

  1. 运行 Redisdocker run -d -p 6379:6379 --name redis-container redis,其中,redis-container为容器名,可通过命令docker container ls-p配置端口映射,将容器内的 6379 端口映射到 Windows 系统的 6379 端口。

  2. 输入docker ps,就可以查看到容器内部已经运行了 Redis

  3. docker ps中,就可以获取到 Redis 运行所在的容器的 id,下面我们进入该容器内部,启动 Redis 客户端,并进行测试:

# 进入 redis 容器
docker exec -it ee0a8f1babe9 /bin/bash
# ping 一下 redis 服务器
root@ee0a8f1babe9:/data# redis-cli
127.0.0.1:6379> ping
PONG # redis-server 回复,表示连通
  1. 下载 Redis 源文件:
git clone https://github.com/antirez/redis.git
  1. 进入 Redis 目录,执行编译:
cd redis
sudo make MALLOC=libc

编译成功后,会在redis/src目录下产生 6 个可执行文件:

name description
redis-server Redis服务端程序
redis-cli Redis 客户端程序
redis-sentinel Redis 哨兵模式(监控和故障转移)
redis-benchmark Redis 性能检测
redis-check-aof 和 redis-check-dump 这两个可执行程序对于不常见的数据文件损坏修复很有用
  1. 可选步骤:编译成功后,可以选择运行测试用例,保证编译成功:
# 测试需要安装 tcl
sudo apt-get update
sudo apt-get install build-essential tcl
# 进行测试
sudo make test
  1. 安装 Redis:通常将 redis-serverredis-cli 复制到/usr/local/bin即可:
sudo cp src/redis-server /usr/local/bin/
sudo cp src/redis-cli /usr/local/bin/

或者直接使用以下命令进行安装:

sudo make install
  1. 配置 RedisRedis 安装完成后,就可以对它进行一些配置,首先需要创建一个配置目录,通常为/etc/redis,并创建一个配置文件redis.conf
sudo mkdir /etc/redis
sudo cp redis/redis.conf /etc/redis
  1. 编辑 Redis 配置文件,修改如下内容:
# supervised no
supervised systemd  # 因为 Ubuntu 使用 systemd 作为系统启动初始化系统
# dir ./
dir /var/lib/redis  # 保存持久化数据文件目录
  1. 新建 Redis 服务进程系统启动文件:
# 首先打开 redis.service 文件进行编辑
sudo vim /etc/systemd/system/redis.service
# 添加一些描述
[Unit]
Description=Redis In-Memory Data Store # 描述
After=network.target # 启动此服务之前网络必须可用
# 指定服务的行为
[Service]
User=redis # 服务所属用户
Group=redis # 服务所属组
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf # redis 服务启动
ExecStop=/usr/local/bin/redis-cli shutdown # redis 服务停止
Restart=always # 配置 redis 尽快能从失败中恢复
# 定义服务应该附加到的 systemd 目标
[Install]
WantedBy=multi-user.target
  1. 由于步骤7中配置了 Redis 服务进行归属于 redis 用户和 redis 组,因此这里需要创建用户和组:
sudo adduser --system --group --no-create-home redis
sudo mkdir /var/lib/redis
sudo chown redis:redis /var/lib/redis  # 更改文件目录拥有者属性
sudo chmod 770 /var/lib/redis          # 更改文件目录读写模式,只允许 redis 用户及超级管理员
  1. 启动和测试 Redis
sudo systemctl start redis  # 启动 redis 服务
sudo systemctl status redis # 检查 redis 服务是否有错误

:步骤5 到 步骤9 所做的其实本质上就是为了启动 redis-server,如果嫌麻烦,直接命令行启动 redis-server 也可以。

  1. 步骤9完成后,理论上 redis-server 就已经启动了,那么此时我们就可以启动客户端 redis-cli,与 redis-server 进行通信:
why8n@VM-0-11-ubuntu:/$ redis-cli     # 启动 redis 客户端
127.0.0.1:6379> ping                  # ping 服务端,查看是否连通
PONG                                  # 服务端回复 PONG,表示连通
127.0.0.1:6379> set test "store data" # 设置键
OK
127.0.0.1:6379> get test              # 获取键值
"store data"
127.0.0.1:6379> exit                  # 退出客户端
why8n@VM-0-11-ubuntu:/$ redis-cli     # 重新开启一个 redis 客户端
127.0.0.1:6379> get test              # 获取前面设置的键值
"store data"
127.0.0.1:6379> exit
  1. 通信成功后,最后设置 Redis 开机启动:
sudo systemctl enable redis

配置文件常用配置项

Redis 支持直接启动,此时使用的是 Redis 内置的默认配置,但直接启动只建议用于测试和开发环境中使用。

在项目部署启动时,建议加上指定配置文件:

redis-server /etc/redis/redis.conf

其中,/etc/redis/redis.conf为配置文件,常用的配置项如下表所示:

配置项 描述
daemonize no 配置是否已守护进程方式运行。其值有:
yes: 以守护进程方式运行
no:以普通进程运行
pidfile /var/run/redis.pid 指定当 Redis 以守护进程方式运行时,写入其 pid 的文件。
port 6379 指定 Redis 监听端口。默认端口号为 6379。
bind 127.0.0.1 设置绑定主机地址
timeout 0 指定超时时间(单位:秒),当客户端闲置超过该时间后,关闭客户端连接。0表示关闭超时,即永久不超时。
loglevel notice 指定日志记录级别。Redis 工支持四个日记记录级别:debugverbosenoticewarning,默认为notice
logfile stdout 配置日志记录方式,默认为标准输出stdout。如果在标准输出模式下,Redis 以守护进程方式运行,则日志会被输出到dev/null
database 16 设置数据库数量,默认为0
save <seconds> <changes> 指定在多长时间段内,超过多少次更新,则将数据库持久化到磁盘中
dbfilename dump.rdb 指定持久化本地数据库文件名,默认为 dump.rdb
appendonly no 指定是否在每次更新操作后进行日志记录,默认为no
appendfilename appendonly.aof 指定更新日志文件名,默认为 appendonly.aof
appendfsync everysec 指定更新日志条件,共有 3 个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折中,默认值)
dir ./ 指定本地数据库存放目录
slaveof <masterip> <masterport> 设置主从数据库复制,设置当本机为 slave 服务时,指定 master 服务的 IP 及 端口,在 Redis 启动时,它会自动从 master 进行数据同步
masterauth <master-password> 设置 master 服务密码
requirepass foobared 设置 Redis 连接密码,此时客户端进行连接时,需要使用 AUTH <password>命令进行连接
maxclients 128 设置同一时间最大客户端连接数。默认为0,表示不限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回max number of clients reached错误信息
maxmemory <bytes> 设置 Redis 最大内存限制
include /path/to/local.conf 加载其他配置文件

更多配置选项,请查看:redis.conf

:通过修改redis.conf文件或使用CONFIG set命令来修改配置,可以通过CONFIG命令查看或设置配置项:

127.0.0.1:6379> config set loglevel "notice" # 配置 loglevel
OK
127.0.0.1:6379> config get loglevel # 查询配置 loglevel
1) "loglevel"
2) "notice"

常用命令

Redis 提供了一些命令,让我们可以对数据进行一些增删改查操作。

这里我们通过客户端(redis-cli)进行操作,请确保执行命令前 Redis 服务端(redis-server)已启动。

下面介绍常用的命令:

  1. Redis 支持常见的 5 种数据类型:String,List,Hash,Set,zset(Sorted Set)

set key value,添加字符串数据
示例:

127.0.0.1:6379> set name whyn
OK

get key,查询键为 key 的值
示例:

127.0.0.1:6379> get name
"whyn"

:格式同

del key,删除键为 key 的数据
示例:

127.0.0.1:6379> del  name
(integer) 1
127.0.0.1:6379> get name
(nil)

:String 类型的值最大能存储 512MB。

:添加一个或多个值到列表头部(左插)或尾部(右插):
1)将一个或多个值插入到列表头部:LPUSH key value1 [value2]

127.0.0.1:6379> lpush list1 value1 value2
(integer) 2

2)将一个或多个值插入到列表尾部:RPUSH key value1 [value2]

127.0.0.1:6379> rpush list1 value3 value4
(integer) 4

3)将一个值插入到列表头部,如果列表不存在,插入无效:LPUSHX key value
4)将一个值插入到列表尾部,如果列表不存在,插入无效:RPUSHX key value

:查询列表
1)通过索引获取列表中的元素:LINDEX key index

127.0.0.1:6379> lindex list1 0  # 查询 list1 列表第1个元素
"value2"
127.0.0.1:6379> lindex list1 -1 # 查询 list1 列表最后一个元素
"value4"

2)指定列表范围获取列表中的元素:LRANGE key start stop

127.0.0.1:6379> lrange list1 0 -1 # list1列表的第1个到最后一个,即全部元素
1) "value2"
2) "value1"
3) "value3"
4) "value4"

3)获取列表长度:LLEN key

:更改列表元素的值
1)通过索引设置列表元素的值:LSET key index value
2)在列表的指定元素前或者后插入元素:LINSERT key BEFORE|AFTER pivot value

127.0.0.1:6379> rpush mylist "Hello" "World"
(integer) 2
127.0.0.1:6379> lrange mylist 0 -1
1) "Hello"
2) "World"
127.0.0.1:6379> linsert mylist before "World" "My" # 在 World 之前插入 My
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "Hello"
2) "My"
3) "World"

:删除列表元素
1)移除并获取列表的第一个元素:LPOP key
2)移除列表的最后一个元素,返回值为移除的元素:RPOP key
3)对一个列表进行修剪(trim),只保留指定区间的元素:LTRIM key start stop
4)移除并获取列表的第一个元素, 如果列表为空,则会 阻塞 直到有新元素添加或超时:BLPOP key1 [key2 ] timeout
5)移除并获取列表的最后一个元素,如果列表为空,则会 阻塞 直到有新元素添加或超时:BRPOP key1 [key2 ] timeout

:添加一个或多个键值对:
1)添加一个键值对:HSET key field value

127.0.0.1:6379> hset hash1 field1 value1
(integer) 1

2)添加多个键值对:HMSET key field1 value1 [field2 value2 ]

127.0.0.1:6379> hmset hash2 field2 value2 field22 value22
OK

:对 key 进行查询:
1)查询 key 对应的哈希表中指定字段的值:HGET key field

127.0.0.1:6379> hget hash1 field1
"value1"

2)查询 key 对应的哈希表所有的字段和值:HGETALL key

127.0.0.1:6379> hgetall hash2
1) "field2"
2) "value2"
3) "field22"
4) "value22"

3)查询 key 对应的哈希表的所有字段:HKEYS key

127.0.0.1:6379> hkeys hash2
1) "field2"
2) "field22"

4)获取所有给定字段的值:HMGET key field1 [field2]

127.0.0.1:6379> hmget hash2 field2 field22
1) "value2"
2) "value22"

5)查看哈希表 key 中,指定的字段是否存在:HEXISTS key field

Redis 中每个 Hash 可以存储 2^{32} -1 个键值对(40多亿)。

:格式同

:删除 key 对应哈希表的一个或多个字段:HDEL key field1 [field2]

:添加一个或多个值到集合中:SADD key member1 [member2]

127.0.0.1:6379> sadd set1 member1 member2
(integer) 2

:查询 key 对应集合中的元素:
1)返回集合中的所有成员:SMEMBERS key

127.0.0.1:6379> smembers set1
1) "member2"
2) "member1"

2)判断 member 元素是否是 key 集合的成员:SISMEMBER key member

127.0.0.1:6379> sismember set1 member
(integer) 0  # 不是成员
127.0.0.1:6379> sismember set1 member1
(integer) 1  # 是成员

3)获取集合元素大小:SCARD key

127.0.0.1:6379> scard set1
(integer) 2

4)差集:获取多个集合间的差集(不同部分):SDIFF key1 [key2]

127.0.0.1:6379> sadd set1 member1 member2
(integer) 2
127.0.0.1:6379> sadd set2 member2 member22
(integer) 2
127.0.0.1:6379> sdiff set1 set2    # set1与set2的差集
1) "member1"

5)交集:获取多个集合间的交集(共有元素):SINTER key1 [key2]

127.0.0.1:6379> sinter set1 set2
1) "member2"

6)并集:获取多个集合间的并集:SUNION key1 [key2]

127.0.0.1:6379> sunion set1 set2
1) "member2"
2) "member22"
3) "member1"

:格式同

:删除 key 对应集合中的元素:
1)移除集合中一个或多个成员:SREM key member1 [member2]
2)移除并返回集合中的一个随机元素:SPOP key [count]

Redis 中每个 Set 集合中最大的成员数为2^{32} - 1(4294967295, 每个集合可存储 40多亿个成员)。

:添加一个或多个值到有序集合中:
1)向有序集合添加一个或多个成员,或者更新已存在成员的分数:ZADD key score1 member1 [score2 member2]

127.0.0.1:6379> zadd zset1 1 member1 2 member2
(integer) 2

:查询有序集合数据
1)通过索引区间返回有序集合指定区间内的成员(默认按分数(score)从低到高排序):ZRANGE key start stop [WITHSCORES]

127.0.0.1:6379> zrange zset1 0 -1
1) "member1"
2) "member2"

2)通过索引区间返回有序集合指定区间内的成员,按分数(score)从高到低排序:ZREVRANGE key start stop [WITHSCORES]

127.0.0.1:6379> zrevrange zset1 0 -1
1) "member2"
2) "member1"

3)通过分数(score)返回有序集合指定区间内的成员:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]

# 返回分数为[1,10]之间的成员
127.0.0.1:6379> zrangebyscore zset1 0 10
1) "member1"
2) "member2"

4)返回有序集中指定分数(score)区间内的成员,按分数(score)从高到低排序:ZREVRANGEBYSCORE key max min [WITHSCORES]

127.0.0.1:6379> zrevrangebyscore zset1 10 0
1) "member2"
2) "member1"

5)返回有序集中,成员的分数值:ZSCORE key member

127.0.0.1:6379> zscore zset1 member1
"1"
127.0.0.1:6379> zscore zset1 member2
"2"

6)返回有序集合中指定成员的索引:ZRANK key member

127.0.0.1:6379> zrank zset1 member2
(integer) 1

7)获取有序集合大小:ZCARD key

127.0.0.1:6379> zcard zset1
(integer) 2

8)获取有序集合在指定分数区间的元素个数:ZCOUNT key min max

127.0.0.1:6379> zcount zset1 0 10
(integer) 2

:格式同

:删除有序集合一个或多个成员数据
1)移除有序集合中的一个或多个成员:ZREM key member [member ...]
2)移除有序集合中给定的分数区间的所有成员:ZREMRANGEBYSCORE key min max
3)移除有序集合中给定的排名区间的所有成员:ZREMRANGEBYRANK key start stop

  1. 对键进行操作的命令:
127.0.0.1:6379> keys * # 查找所有键
1) "name"
127.0.0.1:6379> type name
string
  1. 对数据库进行操作的命令Redis 可以支持多个数据库操作,比如database 16,表示支持 16 个数据库,各个数据索引为:0~15。
  1. 发布-订阅Redis 支持发布-订阅(pub/sub)消息通信模式,其执行模型为:一个频道(channel)可以被一个或多个 Redis 客户端进行订阅,当有新消息通过PUBLISH命令发送给该频道时,这个消息就会被发送给订阅该频道的所有客户端。具体模型如下图所示:
pub/sub

Redis 发布-订阅相关命令如下所示:

更多其他相关命令,请查看:commands#pubsub

示例:启动三个客户端(redis-cli),其中两个订阅频道chatChannel,剩余最后一个客户端发布消息到chatChannel频道,查看订阅者是否能接收到该信息。
具体步骤如下:
1)首先启动3个客户端(redis-cli),其中两个订阅频道chatChannel

subscrieb chatChannel

2)剩余一个客户端发布消息到频道chatChannel,此时另外两个客户端应该能接收到这条消息:

publish chatChannel hello
  1. Redis 事务Redis 是单线程运行模型,其单个操作是具备原子性的,而对于多个操作,Redis 也提供了事务支持,使多个操作也具备原子性。

Redis 事务执行模型:开启事务时,Redis 并不会直接运行命令,而是将命令序列号并放入队列缓存,在执行事务(EXEC)时,会依次执行队列中命令,整个队列执行是一个原子操作,队列中的命令要么依次按顺序执行,要么就完全不执行。

一个典型的 Redis 事务会经历 3 个过程:开始事务(MULTI命令入队执行事务(EXECRedis 事务可以一次执行多个命令,并且同时带有以下 3 个保证:

Redis 事务相关命令如下所示:

示例:开启一个事务,实现用户A转账 100 元给到用户B。
具体步骤如下:

127.0.0.1:6379> set A 100    # 创建用户A
OK
127.0.0.1:6379> set B 0      # 创建用户B
OK
127.0.0.1:6379> multi        # 开启事务
OK
127.0.0.1:6379> get A        # 获取 A
QUEUED                       # get A 入队成功
127.0.0.1:6379> get B
QUEUED                       # get B 入队成功
127.0.0.1:6379> decrby A 100 # A-100
QUEUED                       # decrby A 100 入队成功
127.0.0.1:6379> incrby B 100 # B+100
QUEUED                       # incrby B 100 入队成功
127.0.0.1:6379> get A        # 再次获取 A 的值
QUEUED                       # get A 入队成功
127.0.0.1:6379> get B        # 获取 B 的值
QUEUED                       # get B 入队成功
127.0.0.1:6379> exec         # 执行事务
1) "100"                     # get A
2) "0"                       # get B
3) (integer) 0               # decrby A 100
4) (integer) 100             # incrby B 100
5) "0"                       # get A
6) "100"                     # get B

Redis 的事务与传统的关系型数据库的事务不太一样。Redis 事务中可能出现两种错误类型:

127.0.0.1:6379> set key1 hello                                          # 创建键 key1 = hello
OK
127.0.0.1:6379> multi                                                   # 开启事务
OK
127.0.0.1:6379> set key1 hi                                             # 修改键 key1 = hi
QUEUED
127.0.0.1:6379> adasdfasdfasdf                                          # 无法识别的指令
(error) ERR unknown command `adasdfasdfasdf`, with args beginning with:
127.0.0.1:6379> get key1                                                # 获取键
QUEUED
127.0.0.1:6379> exec                                                    # 执行事务
(error) EXECABORT Transaction discarded because of previous errors.     # 事务执行失败
127.0.0.1:6379> get key1
"hello"                                                                 # 获取键值为 hello,表示事务执行确实失败,未能更改 key1 的值
127.0.0.1:6379> set key1 hello                         # 创建 key1 = hello
OK
127.0.0.1:6379> multi                                  # 开启事务
OK
127.0.0.1:6379> set key1 hi                            # 修改 key1 = hi
QUEUED
127.0.0.1:6379> incrby key1 10                         # key1+=10,字符串无法进行加减,此会命令执行会失败
QUEUED
127.0.0.1:6379> get key1                               # 获取 key1
QUEUED
127.0.0.1:6379> exec                                   # 执行事务
1) OK                                                  # set key1 hi 执行成功
2) (error) ERR value is not an integer or out of range # incrby key1 10 执行失败
3) "hi"                                                # get key1 执行成功
127.0.0.1:6379> get key1                               # key1 = "hi",说明事务执行成功
"hi"

:区分 Redis 事务会不会 回滚,主要看事务中的命令是否能放入队列中,如果都能,则不管命令执行是否成功,都不会进行回滚。反之,只要出现无法入队的指令,Redis 就会回滚整个操作。

由于多个客户端可以对 Redis 数据库的同一个 key 进行操作,因此,在某个客户端执行事务前,数据可能存在偏差(被其他客户端进行修改),导致结果出现异常。Redis 为了解决上述问题,为我们提供了WATCH命令,可以让我们对一个或多个 key 进行监控,确保相应的 key 未被意外修改后,才执行事务。

示例:开启两个客户端,在客户端A 内监控键watchedKey,并开启一个事务,修改键watchedKey。客户端B 在客户端A 执行事务前,修改下键watchedKey,查看运行效果。
具体步骤如下:
1)创建键watchedKey,随意赋一个值:

127.0.0.1:6379> set watchedKey hello   
OK   

2)客户端A 监控键watchedKey,并开启一个事务,修改watchedKey

127.0.0.1:6379> watch watchedKey
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set watchedKey HELLO
QUEUED

3)此时,客户端B 修改键watchedKey

why8n@VM-0-11-ubuntu:~$ redis-cli
127.0.0.1:6379> set watchedKey hi
OK

4)客户端A 执行事务,查看结果:

127.0.0.1:6379> exec
(nil) # 表示事务被打断
127.0.0.1:6379> get watchedKey
"hi" # 事务没有被执行,因此 watchedKey != HELLO

可以看到,在我们执行事务前,如果监控的 key 被修改了,那么事务不会执行。

  1. 其他命令:
127.0.0.1:6379> ping
PONG
# 连接到主机为 127.0.0.1,端口为 6379 ,密码为 mypass 的 redis 服务上
redis-cli -h 127.0.0.1 -p 6379 -a "mypass"
127.0.0.1:6379> config get dir
1) "dir"
2) "/var/lib/redis"

可以看到我们的 Redis 安装目录为:/var/lib/redis

更多命令,请参考:commands

Redis 过期策略

Redis 中,我们可以使用EXPIRE命令设置数据的过期时间。

Redis 使用两种策略检测数据是否过期:惰性删除 + 定期删除

惰性删除 由于只在访问的时候才进行检测,因此可能存在许多未访问但已过期的数据,占用大量的内存。

定期删除 由于是抽样检测,因此也存在很多过期 Key 到了时间并没有被删除掉的可能,同样也会导致大量的内存被占用。

因此,使用 Redis 过期策略只能在一定程度上缓解 Redis 的内存占用问题,还是可能出现内存耗尽的情况,而当出现这个情况的时候,Redis 就会走 内存淘汰策略 释放内存空间。

Redis 内存淘汰策略

Redis 用作数据缓存时,其内存占用最大空间可由maxmemory进行配置:

# redis.conf
maxmemory 100mb # 配置内存最大占用为 100M
maxmemory 0 # 0表示内存占用无限制,以机器内存空间为准

当内存占用达到maxmemory指定最大限制时, Redis 会根据不同的数据淘汰策略采取不同的操作,可以通过maxmemory-policy配置采取的策略,共有如下 8 种不同的策略:

:为了能更好地使用内存,建议在平时使用 Redis 时,应尽量主动设置/更新 key 的 expire 过期时间,主动剔除不活跃的数据,提升查询性能。

Redis集群

Redis集群 提供了在分布式环境中多节点数据自动分布/共享机制,并且其还具备高可用性(master-slave 机制:主从复制、主从切换),当某个 master节点 失效时,其 slave节点 可以升级为 master节点,确保了整个 Redis集群 能正常运作。当然,如果 master节点 和 其所有 slave节点 在同个时间点内都失效了,那么 Redis集群 系统就会不可用。

redis-cluster 架构

Redis集群 架构图中:蓝色圈圈代表 Redis集群节点,可以看到,客户端进行请求时,该请求会在集群节点中随意跳转,直到找到能回复该客户端请求的节点。

上图集群共有 5 个节点,假如此时黄色节点向红色节点发出 ping 命令,但是红色节点没有回应,则此时黄色节点就会认为红色节点挂掉了,然后投上一票,同时将红色节点疑是挂掉广播给其他节点,其他节点就会去 ping 一下红色节点,发现无法通信时也会投上一票,当该集群中超过 3 个及以上节点投票认为红色节点挂掉时,红色节点就被认为失效了。如果此时红色节点有从节点,那么其某个从节点就会被提升为主节点,替换该红色节点,进行故障转移。如果红色节点没有从节点,那么由于哈希槽缺失了部分,将导致整个集群系统不可用。

:投票容错机制 其实就是 多数仲裁机制,因此,一个集群的搭建最少需要 3 个主节点(mater)。

对哈希槽的移动操作,不会影响 Redis 的其他操作,两者是并行运行的。也就是说,在 Redis集群 运行时,我们可以通过移动哈希槽的方式(无须停机操作),动态地增加或删除节点,或者更改节点间的哈希槽占比,而无须停止 Redis 的其他操作。

  1. 创建一个redis-cluster目录,在该目录内分别创建目录7000 ~ 7005共 6 个节点目录:
mkdir redis-cluster
cd redis-cluster
mkdir 7000 7001 7002 7003 7004 7005 
  1. 7000 ~ 7005每个目录内创建一个conf目录,在conf目录内创建配置文件redis.conf
$ for port in `seq 7000 7005`; do \
mkdir -p ./${port}/conf \
&& echo "port "${port} >> ./${port}/conf/redis.conf \
&& echo "cluster-enabled yes" >> ./${port}/conf/redis.conf \
&& echo "cluster-config-file nodes.conf" >> ./${port}/conf/redis.conf \
&& echo "cluster-node-timeout 5000" >> ./${port}/conf/redis.conf \
&& echo "appendonly yes" >> ./${port}/conf/redis.conf
done
  1. 下载 Redis 镜像:
docker pull redis
  1. docker 启动 reidis,启动时加载相应的配置文件:
$ for port in `seq 7000 7005`; do \
docker run \
--net=host \
-v ~/redis-cluster/${port}/conf/redis.conf:/usr/local/etc/redis/redis.conf \
--name redis-${port} \
-d redis redis-server /usr/local/etc/redis/redis.conf; \
done
  1. 进入任意一个 redis 容器中,启动客户端,创建集群。创建集群的时候需要输入yes
$ docker exec -it redis-7000 /bin/bash
$ redis-cli --cluster create \
127.0.0.1:7000 \
127.0.0.1:7001 \
127.0.0.1:7002 \
127.0.0.1:7003 \
127.0.0.1:7004 \
127.0.0.1:7005 \
--cluster-replicas 1
创建集群
  1. 到这里,我们就完成了 Redis集群 的部署。我们可以随意登录一个 redis 容器,查看相关信息:
$ docker exec -it redis-7000 redis-cli -c -p 7000 # 注意端口号
127.0.0.1:7000>info replication # 查看主从节点信息
127.0.0.1:7000>cluster info # 查看集群信息

:使用 redis 容器内的客户端应当加上-p参数指定端口,否则会使用默认6379端口的客户端,该客户端我们并未进行集群配置。

结果如下:

info replication cluster nodes
  1. 下面我们进行存值测试:
$ docker exec -it redis-7000 redis-cli -c -p 7000           
127.0.0.1:7000> set a 1                                                            
-> Redirected to slot [15495] located at 127.0.0.1:7002                             
OK                                                                         
127.0.0.1:7002> keys *                                           
1) "a"                                                            
127.0.0.1:7002>exit
$ docker exec -it redis-7001 redis-cli -c -p 7001
127.0.0.1:7001> keys *
(empty list or set)
127.0.0.1:7001> get a
-> Redirected to slot [15495] located at 127.0.0.1:7002
"1"
    

可以看到,我们在127.0.0.1:7001设置了a=1,由于键a的哈希槽为15495,位于节点127.0.0.1:7002,因此我们被重定向到了节点127.0.0.1:7002。随意进入另一个 redis 节点,进行取值操作,可以看到我们同样被重定向到了节点127.0.0.1:7002
:注意需要使用-c参数来启动集群模式。

其他语言客户端集成

由于 Redis 是采用基于 C/S 架构,且 Client 和 Server 之间采用 Socket 通信,因此,除了使用默认的redis-cli命令行客户端外,也可以很容易集成其他语言编写的客户端程序。

一个需要注意的点是,Redis 客户端和服务器之间通信采用了一套自定义协议规范: RESP (REdis Serialization Protocol,Redis 序列化协议)。

RESP 可以对 整型字符串数组 等多种不同类型数据进行序列化,客户端通过 Socket 请求将命令以字符串数组形式发送给服务端执行。

简单来说,RESP 对不同的类型表示如下:

关于 RESP 更详细内容,可参考官网文档:Redis Protocol specification

:实际上,Redis 底层支持多种不同协议,除了 RESP 外,还支持 inline command 等协议格式,比如,telnet使用的就是 inline command 协议:

$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
set hello world # 设置
+OK
get hello       # 获取
$5
world
quit            # 退出 telnet
+OK
Connection closed by foreign host.

此外,Redis 集群也是采用一套不同于 RESP 的二进制协议规范,因为他需要将消息在不同的结点间进行交换。

下面,我们采用 Java 语言实现一个简单的 Redis 客户端,可以完成字符串键值对设置:

import java.io.*;
import java.net.*;

public class RedisClient {
    private Socket socket;

    public RedisClient(String host, int port) {
        try {
            this.socket = new Socket(host, port);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void close() {
        if(this.socket != null) {
            try{
                this.socket.close();
                this.socket = null;
            }catch(IOException e) {
                e.printStackTrace();
            }
        }
    }

    // build the command using RESP protocol
    private String buildComnand(String command) {
        // set key value
        String[] commandItems = command.split("\\s+");
        StringBuilder builder = new StringBuilder();
        builder.append(String.format("*%d\r\n", commandItems.length));
        for (String item : commandItems) {
            builder.append(String.format("$%d\r\n%s\r\n", item.length(), item));
        }
        return builder.toString();
    }

    // set key value
    public String set(String key, String value) throws IOException {
        String command = this.buildComnand(String.format("set %s %s", key, value));

        OutputStream out = this.socket.getOutputStream();
        out.write(command.getBytes("UTF-8"));
        out.flush();

        InputStream in = this.socket.getInputStream();
        byte[] data = new byte[1024];
        int len = in.read(data);
        return new String(data, 0, len);
    }

    // get key
    public String get(String key) throws IOException {
        String command = this.buildComnand("get " + key);
        this.socket.getOutputStream().write(command.getBytes("UTF-8"));

        InputStream in = this.socket.getInputStream();
        byte[] data = new byte[1024];
        int len = in.read(data);
        // $5\r\nVALUE\r\n
        return new String(data, 0, len).split("\r\n")[1];
    }

    public static void main(String[] args) throws IOException {
        RedisClient client = new RedisClient("127.0.0.1", 6379);

        System.out.println("set name Whyn");
        String response = client.set("name", "Whyn");
        System.out.println(response);      // +OK

        System.out.println("get name");
        String value = client.get("name");
        System.out.println(value);         // Whyn

        client.close();
    }
}

上述例子只是一个简单的 Demo 展示程序,主要用于展示实现原理。实际使用时,选择相关开源框架即可,比如 Jedis、Lettuce...相关集成内容,可参考笔者的另一篇文章:Spring Boot - 集成 Redis

参考

上一篇下一篇

猜你喜欢

热点阅读