本文主要描述Redis大致的使用方式,为读者对Redis的使用有一定的全局观。更为细节的内容,可通过查询相关书籍或博客深入了解。

由于Redis很好地支持了大多数流行的编程语言,实际上大多在各自的程序中对Redis进行操作,因此,在了解Reids本身的操作后,另外需要花费少量时间了解对应语言对应的库的使用

Redis为C语言的开源项目,若是已经对Redis又深入的了解,可通过阅读Redis源码获得更深层次的认识,或根据自身需求做修改(自用可以,但实际工作不建议随意修改)。

1 基础知识

1.1 登录

redis的命令不区分大小写。但之后的值匹配中需要注意大小写。 启动可直接在终端输入 redis-server。或者通过脚本运行,该脚本在redis安装目录下的 utils文件夹中,名为 redis_init_script,在文件中可修改对应的配置信息,并重命名复制目录/etc/init.d,通过该脚本可启动一个redis实例,通过下方命令可设置开机启动

当复制多个文件,并修改为不同的端口号后,便可同时运行多个redis实例。默认的端口号为6379。 启动后,首先登录redis:

终端输入redis-cli,开启redis,并输入自己的密码,否则无法操作。 登录远程redis服务器:

需要停止时:

1.2 插入与查询

Redis系统中,不同于MySQL之类的关系型数据库,不仅数据保存在内存中,且数据记录以字典结构保存(MySQL中的存储结构多为B+树的形式,但根据引擎的不同有所区别)。 在MySQL中可以创建多个自定义命名的数据库,并在其中创建多个表,在表内存放一行行的数据。

为了记录信息,首先需选择对应的数据库,在数据库中创建新的键,再按照对应的方法插入数据。默认在登录的时候处于0数据库。

查询数据库中存在的键,使用命令 keys,后跟匹配模式,例如 keys * 显示所有键。

类型命令
字符串set 键名 键值
集合sadd 键名 键值
散列表hset 键名 域名 域值

hmset 键名 域名 域值 域名 域值
有序集合zadd 键名 成员 分值
列表lpush 或 rpush (从左边或右边推入新值)
类型命令
字符串get 键名
集合smembers 键名
散列表hget 键名 域名

hgetall 键名(返回域名与对应的域值)

hkeys 键名(返回所有的域键)
有序集合zrange 键名 起始位置 结束位置

zrangebyscores 键名 分值下限 分值上限
[withscores]
列表lrange 键名 起始位置 结束位置

lindex 键名 位置
类型命令
字符串strlen
集合scard
散列表hlen
有序集合zcard
列表llen

1.3 删除与判断

通过上述的介绍,已经明显看出由于类型的不同导致命令的区别。同样的,对元素的删除,也需要不同的命令,以及判断键值中是否存在给定的值。 删除操作:

类型命令判断命令
字符串del 键名 
集合srem 键名 给定值sismeber 键名 给定值
散列表hdel 键名 域名hexists 键名 给定域名
有序集合zrem 键名 成员名 
列表lpop 或 rpop
(从左边或右边弹出元素)
 

删除数据库中的键统一用 del操作

但redis中只能一个个地删除,一旦需要删除一堆长相类似的键则需要结合Linux的命令

最后,可用命令 dbsize查询当前数据库中键的数量。

1.4 数据操作

1.4.1 加减

|类型|加|减| ||| |字符串|incr 键名(加1)

incrby 键名 给定增量

incrbyfloat 给定增量
(对数值型的字符串操作)|decr 或 decrby| |集合||| |散列表|hincrby 或 hincrbyfloat|| |有序集合|zincrby || |列表|||

其中集合还包含交并集操作

操作并集交集差集
简单命令sunionsintersdiff
结果保存sunionstore 新集合键名
键名1 键名2
sinterstoresdiffstore
1.4.2 期限

由于Redis的数据保存在内存中,企业大多用其存储热点信息。但考虑到内存容量和工作效率,需要对旧数据作清理,于是需要对数据设定一个存活时长,令数据在经历或到达一个时间后自发删除。

命令操作描述
expireexpire 键名 秒数经历过对应的秒数后,该键删除
expireatexpireat 键名 时间戳参数为UNIX时间戳,到达该时间后,删除该键
pexpire
pexpireat
与上面命令操作类似时间参数以毫秒为单位
TTLTTL 键名返回该键的剩余时长
persistpersist 键名取消键的期限设置

上述介绍了如何手动设置键的存活时长。但在实际中,例如微博或其它社交平台,总是会出现新的热点,而这些热点又很难确定何时消退热度,但为了保证内存不至于被这类所谓的热点塞满,需要系统自行删除一些数据。于是,就有了缓存策略。

规则如下:

规则说明
volatile-lru对设置了期限的键按照LRU算法删除一个
allkeys-lru对任意键按照LRU删除一个
volatile-random对具有期限的键随机删除一个
allkeys-random随机删除一个键
volatile-ttl删除过期时间最近的一个键
noeviction不删除,只返回错误

其中,LRU(Least Recent Used)意为最近最少使用

1.4.3 排序

sort的完整命令如下:

在介绍[BY pattern]之前,需要准备一个例子, 数据库中存在三种键,

类型内容组成
列表用户序号uid:[1,2,3,4]
字符串用户名user_name_1:admin
user_name_2:jack
user_name_3:pete
user_name_4:mary
字符串用户等级user_level_1:9999
user_level_2:10
user_level_3:25
user_level_4:70

这三种键实际是分类保存了用户的信息,而 uid常用来表示实际的用户。但为了获取该用户的其它信息,则需要与其它键建立关系,联系的方式,则是通过模式匹配键值

user_level_* 是一个占位符, 它先取出 uid 中的值, 然后再用这个值来查找相应的键。

比如在对 uid 列表进行排序时, 程序就会先取出 uid 的值 1 、 2 、 3 、 4 , 然后使用 user_level_1 、 user_level_2 、 user_level_3 和 user_level_4 的值作为排序 uid 的权重。

比如说, 以下代码先排序 uid , 再取出键 user_name_{uid} 的值:

组合使用 BY 和 GET 通过组合使用 BY 和 GET , 可以让排序结果以更直观的方式显示出来。

比如说, 以下代码先按 user_level{uid} 来排序 uid 列表, 再取出相应的 user_name{uid} 的值:

获取多个外部键 可以同时使用多个 GET 选项, 获取多个外部键的值。

以下代码就按 uid 分别获取 user_level{uid} 和 user_name{uid} :

GET 有一个额外的参数规则,可以用 # 获取被排序键的值。

以下代码就将 uid 的值、及其相应的 user_level* 和 user_name* 都返回为结果:

获取外部键,但不进行排序 通过将一个不存在的键作为参数传给 BY 选项, 可以让 SORT 跳过排序操作, 直接返回结果:

这种用法在单独使用时,没什么实际用处。

不过,通过将这种用法和 GET 选项配合, 就可以在不排序的情况下, 获取多个外部键, 相当于执行一个整合的获取操作(类似于 SQL 数据库的 join 关键字)。

以下代码演示了,如何在不引起排序的情况下,使用 SORT 、 BY 和 GET 获取多个外部键:

将哈希表作为 GET 或 BY 的参数 除了可以将字符串键之外, 哈希表也可以作为 GET 或 BY 选项的参数来使用。

比如说,对于前面给出的用户信息表:

我们可以不将用户的名字和级别保存在 user_name{uid} 和 user_level{uid} 两个字符串键中, 而是用一个带有 name 域和 level 域的哈希表 user_info_{uid} 来保存用户的名字和级别信息:

之后, BY 和 GET 选项都可以用 key->field 的格式来获取哈希表中的域的值, 其中 key 表示哈希表键, 而 field 则表示哈希表的域:

通过给 STORE 选项指定一个 key 参数,可以将排序结果保存到给定的键上。

如果被指定的 key 已存在,那么原有的值将被排序结果覆盖。

可以通过将 SORT 命令的执行结果保存,并用 EXPIRE key seconds 为结果设置生存时间,以此来产生一个 SORT 操作的结果缓存。

这样就可以避免对 SORT 操作的频繁调用:只有当结果集过期时,才需要再调用一次 SORT 操作。

2 事务操作

2.1 multi和exec

与常用数据库类似,事务指一组命令需要被原子执行。但Redis相对于MySQL之类的事务实现简单,也不强大,最重要的是Redis没有回滚操作。

一旦在执行中,某个命令发生错误,则事务中断,但之前已做出的改变无法恢复,需要操作者自行修复。

2.2 管道pipeline

若需要在远程服务器上操作时,需要通过TCP协议建立联系,但是正常的通信总是需要信号不断往返,浪费时间,由此出现pipeline以实现单方面不间断发送命令。

2.3 watch

由于在实际工作中,Redis需要时刻处理大量的操作,若是在事务操作中,其它客户端传入命令将部分数据已修改,则事务操作大概率会出现异常。于是,需要判断一些我们关心的数据是否在这期间被改动,使用命令 watch

在事务操作中,使用该命令相当于监视数据,一旦数据被改动,则事务中断。在执行到 exec后,会自动取消前面的监视,或用命令 unwatch取消。

2.4 锁

watch作为乐观锁,并没有对修改数据的行为作阻止。下面将介绍Redis中悲观锁的使用。 同其它地方的锁的构造相同,通过规定某个变量为锁的条件,只有进程对该变量完成某一操作后,才能进行自己的行为,否则则不断尝试(可设置尝试的时长期限)。

2.4.1 计数信号量

通过计数信号量,可限制进程数量,不同于锁的操作,当进程无法获得锁,则不断尝试,而信号量无法获取,则直接返回失败结果。 为安全考虑,获得信号量的进程同样需要设置期限。

3 安全机制

由于内存一旦断电则数据消失,为保证数据安全 ,首先需要将数据在硬盘中保存备份,称之为持久化。

3.1 持久化

3.1.1 快照持久化

这种方式是直接将整个存储复制到硬盘中,并有两种命令:

命令说明
bgsaveRedis创建一个进程负责复制
saveRedis将暂时停止响应命令
  1. 配置文件中设置save 60 10000,代表从最后一次创建快照算起,若60秒内有10000次写入,则触发bgsave命令。
  2. 若是多个Redis服务器之间通信时,一方向对方发送 sync命令,对方若是最近没有执行快照操作,则执行bgsave命令。
3.1.2 AOF持久化(Append Only File)

由于每次数据的变化都需要利用写命令进行修改,因此,只需要从头到尾保存所有的写命令即可,通过从头到尾执行一遍就可以获得对应的结果。 首先使用AOF功能需要在配置文件中修改appendonly的参数为yes,默认为no。 此外,还有对应的AOF缓冲区同步文件策略,由appendfsync参数控制,

配置参数说明
always每个命令都需要同步写入硬盘(安全但效率低)
everysec每秒执行一次同步,显式地将写命令同步到硬盘
no不主动同步,由系统控制(通常30秒同步一次)

由于AOF文件包含大量的记录,导致文件大小不断膨胀,可通过删减其中的部分记录,减小所需的存储空间,称为重写,为此也可以在配置文件中设置何时执行,

参数说明
auto-aof-rewrite-percentage指明当文献大小与上次重写前的大小百分比达到指定值,后重写
auto-aof-rewrite-min-size文件大小达到指定值,则重写

除了这类自动重写的配置外,还可以通过发送命令 bgrewriteaof手动执行重写。

重写的实现 实际的重写操作不会对原本的文件做分析,而是根据当前的情况,将当前的状况重新写一遍,并删除了之前对键的各种操作,直接用最后的结果代替。

3.2 内存优化

所谓的内存优化,实际对数据结构的优化。Redis中的数据均以列表、集合等数据结构形式保存,这类结构有时为了搜索效率的考虑需要附带额外的信息,导致实际的存储大小远比实际的数据量要大。 为此,当一个数据结构中的数据量较小时,采用额外的数据结构保存,丧失部分的效率,减小存储量。当数据量增大到影响效率后,再恢复到正常的结构。

基于上述的介绍,Redis中对于各个类型值的编码存在多种内部编码,根据用户的需求,可在配置文件中修改,不同编码的应用实时机。

数据类型内部编码方式编码结果
字符串REDIS_ENCODING_RAWraw
 REDIS_ENCODING_INTint
 REDIS_ENCODING_EMBSTRembstr
散列REDIS_ENCODING_HThashtable
 REDIS_ENCODING_ZIPLISTziplist
列表REDIS_ENCODING_LINKEDLISTlinkedlist
 REDIS_ENCODING_ZIPLISTziplist
集合REDIS_ENCODING_HThashtable
 REDIS_ENCODING_INTSETintset
有序集合REDIS_ENCODING_SKIPLISTskiplist
 REDIS_ENCODING_ZIPLISTziplist

当散列类型键的字段个数少于hash-max-ziplist-entries参数值且每个字段名和字段值的长度都小于 hash-max-ziplist-value 参数值(单位为字节)时, Redis 就会使用 REDIS_ENCODING_ZIPLIST 来存储该键,否则就会使用 REDIS_ENCODING_HT。 转换过程是透明的,每当键值变更后Redis都会自动判断是否满足条件来完成转换。


其中,Redis对于字符串,数字等常量值是作为共享值处理,即同样的字符串,它们的地址是相同的。但是,当对Redis的使用内存设置的限制,则不再共享,因为需要为每个值记录其使用率等信息,这涉及到Redis中内存的管理细节。 关于Redis内存的其它细节,可深入了解Redis的内存机制。


4 分布式

4.1 主从链

当业务规模较为庞大后,单台服务器同时完成读写操作较为困难,此时需要将负载分配到多个服务器上。为了保证数据的一致性,读操作可以由多个服务器分担,而写操作则需要一个服务器负责,其它服务器从该服务器获取数据副本。由此,产生主、从关系。

或者在配置文件中,对参数 slaveof <masterip> <masterport>进行配置, 若是主服务器被密码保护,则对参数 masterauth <master-password>进行配置。

此时A开始复制B的数据,若是需要停止复制,命令如下,

通过 info replication命令可查看是否与对应的主或从服务器连接,及其它信息。

4.1.1 哨兵(Setinel)

在主从链中,一个主节点下面有若干个从节点。若主节点出现故障而下线,则最好在从节点中尽快选举出新的主节点,并且保证只有一个从节点转变为主节点。为实现自动化,让系统自动发现故障并修复,则出现哨兵 在设置哨兵后,主从链的每个节点都可以由一个哨兵定期检查,一旦发现主节点故障,多个哨兵会一起协商,推举出新的主节点,并通知用户或客户端。

启动主节点:

从节点 文件:redis-6380.conf(端口号可自行决定) 配置:

若是本机上不同端口对应的不同Redis实例,可用127.0.0.1,若是不同机器上的,则需要指明对应的IP地址 同样利用前述的方式启动

其中,sentinel monitor mymaster指,监控IP:端口所对应的Redis节点,若是需要判断主节点故障,则需要至少给定个数的哨兵都同意才可以。 下面的几个参数,根据其名字可知,down-after-milliseconds mymaster大致为超过给定时间未获得回复,则认为其下线;parallel-syncs mymaster指,当设置了新的主节点后,每次向该主节点发起复制操作的从节点的个数;failover-timeout mymaster为故障转移超时时限,通常是对主节点做故障切换时,若是超过该时限,则认为切换失败,其中,若是本次切换失败,下一次会将时限扩大为双倍。 notification-script,当发生某些警告级别的Sentinel事件,会触发对应目录上的脚本; client-reconfig-script,在故障转移结束后,会触发对应路径的脚本,并向脚本发送故障转移结果的相关参数,当启用该功能时,配置如下:

启动哨兵

或者

4.2 集群

前面的主从链主要通过对数据进行复制,保证数据的安全性。但是当数据本身非常巨大,导致单个服务器已无法妥善保存,则需要多个机器共同保存,由此产生集群。 集群的第一个问题是,数据以何种规则保存在哪个机器上,将机器组成的集群抽象化后,即为在一个大的存储空间上进行分区,问题转化为资源与分区的分配。 常用的方法有:节点取余分区,一致性哈希分区,虚拟槽分区(Redis采用此方法)。

集群功能的限制

1)key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持。 2)key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。 3)key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。 4)不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。 5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

4.2.1 配置

首先创建并启动若干个Redis节点。 每个节点的配置文件中设置参数

在第一次启动后,会根据 cluster-config-file的设置创建一个集群配置文件。 每个节点会生成一个40位的16进制的字符串作为节点ID,保存在各自的集群配置文件中。利用命令 cluster nodes可查看该集群节点的状态。 当前,所有启动的节点并没有组成一个集群

为保证数据安全,同样可以设置主从链,命令 cluster replicate,使用如下:

4.2.2 集群伸缩

5 缓存优化

正如之前已经提及的,实际中的使用常用Redis作为缓存部分,而存储层则使用MySQL等持久层的工具管理。由于存储层的工作效率远低于缓存,因此对于大规模的访问,更希望由缓存负责。如果实际运行后,出现大量的访问需要动用到存储层,不但意味着缓存的设计有问题,也会有极大的可能导致系统宕机。