NoSQL数据库之Redis(实践篇)

x33g5p2x  于2020-10-30 发布在 Redis  
字(18.4k)|赞(0)|评价(0)|浏览(438)

1**、Redis的安装和基本操作**

A**、Redis的安装**

Redis的安装可以说是非常的简单,由于它是使用C语言进行编写的,因此在安装时,需要使用到Linux系统自带的C语言编译器进行编译,这个过程跟先前Hadoop系列其他工具的安装稍有区别。下面正式介绍Redis的安装和配置,这里使用的安装包是在网上下载的redis-3.0.5.tar.gz。

将Redis安装包上传到/root/tools目录下,运行命令tar -zxvf redis-3.0.5.tar.gz -C /root/training/,将Redis安装包解压到/root/training目录下。进入到/root/training/redis-3.0.5目录下,运行命令make,等待编译完成,然后运行命令make PREFIX=/root/training/redis install,将Redis安装到/root/training/redis目录下。

下面进行Redis的配置。Redis的核心配置文件是redis.conf,由于该文件在安装目录下没有,因此需要到源码路径下进行拷贝。进入到/root/training/redis目录下,运行命令mkdir conf,创建conf文件夹,专门用于存放Redis的配置文件。运行命令cp /root/training/redis-3.0.5/redis.conf conf/,从源码目录下拷贝一份redis.conf配置文件到conf/目录下。

进入到/root/training/redis/conf目录下,运行命令 vi redis.conf,编辑redis.conf配置文件,如下图所示,将daemonize no中的no改为yes,表示以后台运行的方式启动Redis,这在生产环境中是必须的。保存退出即可。至此Redis的安装和配置完成。

接下来验证Redis是否安装成功。进入到/root/training/redis目录下,运行命令bin/redis-server conf/redis.conf,启动Redis的Server,再运行命令 bin/redis-cli,启动Redis的客户端,该命令后面没有添加参数,默认将连接到本机的端口6379上。

B**、Redis的基本操作**

在介绍Redis的基本操作之前,先简单介绍下Redis的bin目录下的几个命令脚本,如下图所示。

nredis-benchmark压力测试工具

nredis-check-aof检查AOF日志

nredis-check-dump检查RDB快照文件

nredis-cli启动命令行客户端

nredis-sentinel启动哨兵,实现主从复制的HA(版本2.4之后)

nredis-server启动和停止Redis Server

1)字符串操作

命令:set、get、append;mset(设置多个key-value)、mget(获取多个值)、incr、decr(自增和自减)等

举例如下图所示:

2)链表操作

命令:lpush、lrange、lpop(从左侧对链表进行操作),rpush、rpop(从右侧对链表进行操作)等

举例如下图所示:

3Hash操作

命令:hset、hget、hmset(设置多个hash值,适合用于保存用户信息)、hmget(获取多个hash值)、hgetall(获取所有hash值)

举例如下图所示:

(4)无序集合操作

命令:sadd、smembers、sdiff、sinter、sunion(无序,不可重复的集合)

举例如下图所示:

(5)有序集合操作

命令:zadd、zrange、zrangebyscore、zrevrange、zrevrangebyscore(有序、可以重复的集合,根据一个score来进行排序)

举例如下图所示:

2、Redis的Java程序示例

Redis不仅提供了命令行供用户使用,还提供了Java客户端及对应的API供开发者使用,总的来说,在Java程序中对Redis进行操作比较简单,下面举两个Java程序操作Redis的示例。

A**、字符串操作实例**

测试Java程序对Redis的字符串操作,代码如下图所示:

运行结果如下图所示:

B**、Hash操作实例**

测试Java程序对Redis的Hash操作,代码如下图所示:

运行结果如下图所示:

C**、Redis连接池实例**

虽然Redis已经具备非常高的性能,但在多用户访问的情况下,其性能仍然会受到较大的影响。跟使用其他关系型数据库类似,通过使用Redis自带的连接池功能,能够在较大程度上提高Redis的并发性能。

使用Redis自带的连接池功能,封装成Java工具类的代码如下图所示:

3**、Redis的事务和消息机制**

A**、Redis的事务**

为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis也不例外。Redis的事务使用非常简单,不同于关系型数据库,我们无需理解那么多复杂的事务模型,就可以直接使用。一般情况下Redis在接收到一个client发来的命令后会立即处理并返回处理结果,但是当一个client在一个连接中发出multi命令时,这个连接会进入一个事务上下文,该连接后续的命令并不是立即执行,而是先放到一个队列中,当该连接收到exec命令后,redis会顺序的执行队列中的所有命令,并将所有命令的运行结果打包到一起返回给client,然后此连接就结束事务上下文。

每个事务的操作都有begin、commit和rollback,begin指明事务的开始,commit指明事务的提交,rollback指明事务的回滚,大致的使用形式如下:

begin();

try {

command1();

command2();

....

commit();

} catch(Exception e) {

rollback();

}

Redis****事务的基本使用

Redis在形式上看起来也差不多,分别是multi/exec/discard。multi指明事务的开始,exec指明事务的执行,discard指明事务的丢弃,举例如下图所示(模拟银行转账,从tom转100块钱给kate)。图中的指令演示了一个完整的事务过程,所有的指令在exec之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到exec指令,才开始执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为Redis的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。

原子性

事务的原子性是指,要么事务全部成功,要么全部失败,那么Redis事务的执行是原子性的吗?参考下图实例:

上面的例子,事务执行到中间遇到失败,因为不能对一个字符串进行数学运算,事务在遇到指令执行失败后,后面的指令还会继续执行,所以man的值能够得到设置。到这里,就能明白Redis的事务根本不能算原子性,而仅仅满足了事务的隔离性,隔离性中的串行化——当前执行的事务有着不被其他事务打断的权利。

discard**(丢弃)**

Redis为事务提供了一个discard指令,用于丢弃事务缓存队列中的所有指令,在执行exec之前。举例如下图,可以看到discard指令执行之后,队列中的所有指令都没得到执行,就好像multi和discard中间的所有指令从未发生过一样。

Redis****的锁机制:watch

考虑一个业务场景,当前tom有1000块钱,火车票只有一张。用户一执行如下指令:

set tom 1000

set ticket 1

multi

decr ticket

decrby tom 100

exec

用户二执行如下指令:

decr ticket

没有使用watch指令时,运行结果如下图所示。可以看到,在事务提交前,其他用户对ticket进行了操作,事务得到了正常执行,但ticket的值却出现了异常。

使用了watch指令后,运行结果如下图所示。在事务提交之前,若有其他用户对ticket进行了操作,则事务自动discard。可见,在Redis中的锁是一种乐观锁,它默认为在事务提交前不会有其他用户对watch变量进行操作,否则,就不准许事务执行。

B**、Redis的消息机制**

由于Redis的列表是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的。因此,可以直接使用Redis的list实现消息队列,只需简单的两个指令lpush和rpop或者rpush和lpop。这部分内容这里不做介绍,有兴趣的朋友可以上网学习。

Redis****的SubPub

消息多播允许生产者生产一次消息,中间件负责将消息复制到多个消息队列,每个消息队列由相应的消费组进行消费。它是分布式系统常用的一种解耦方式,用于将多个消费组的逻辑进行拆分。支持了消息多播,多个消费组的逻辑就可以放到不同的子系统中。如果是普通的消息队列,就得将多个不同的消费组逻辑串接起来放在一个子系统中,进行连续消费。

为了支持消息多播,Redis不能再依赖于那5个基本数据类型了,它单独使用了一个模块来支持消息多播,这个模块的名称叫做PubSub,也就是PublisherSubscriber,发布者订阅者模型。Redis PubSub的生产者和消费者是不同的连接,这是必须的,因为Redis不允许连接在subscribe等待消息时还要进行其它的操作。

举例如下图所示,下面的两个小窗口是消息的消费者,必须要先启动起来,消费者启动后,它们就会一直等待监听频道(也叫主题)channel1上的消息,上面的大窗口是消息的发布者,发送完消息后输出了一个数值2,表示有两个消费者监听到了该消息,同时在消费者窗口上也立马显示出了接收到的消息。

模式订阅

上面提到的订阅模式是基于名称订阅的,消费者订阅一个主题必须明确指出主题的名称。如果想要订阅多个主题,那就得同时subscribe多个名称,这样生产者向这些主题发布消息时,消费者就都能够收到。如果现在要增加一个主题,客户端必须也跟着增加一个订阅指令才可以收到新开主题的消息推送。为了简化订阅的繁琐了,Redis提供了模式订阅功能Pattern Subscribe,这样就可以一次订阅多个主题,即使生产者新增加了同模式的主题,消费者也可以立即收到消息。

举例如下图所示,下面的大窗口使用psubscribe模式匹配的方式一次订阅多个主题,主题以channel字符串开头的消息都可以收到,上面两个小窗口依次往频道channel1和channel2上分别发布helloworld和hellochina两个消息,一旦发送结束,大窗口的消费者便接收到了相应的消息。

4**、搭建Redis集群(主从模式)**

这里搭建的Redis集群,并不是真正意义上的集群,它是在一台主机上运行多个Redis进程来模拟的,效果跟生产环境中的Redis集群基本一样,因此只需要在主机hadoop221上进行配置即可。下面介绍如何在主机hadoop221上搭建Redis集群。

之前已经配置好了一份redis.conf配置文件,现在将其进行复制得到另外三份配置文件,分别命名为redis6379.conf(主节点)、redis6380.conf(从节点)、redis6381.conf(从节点),进入到/root/training/redis/conf目录下,依次运行如下命令:

cp redis.conf redis6379.conf

cp redis.conf redis6380.conf

cp redis.conf redis6381.conf

然后,分别对这三份配置文件进行修改,参考如下:

主节点:关闭RDB和AOF

redis6379.conf**:**

/#save 900 1

/#save 300 10

/#save 60 10000

appendonly no

从节点:开启RDB和AOF,参数slaveof用于配置主节点的地址

redis6380.conf**:**

port 6380

dbfilename dump6380.rdb

appendfilename "appendonly6380.aof"

slaveof localhost 6379

redis6381.conf**:**

port 6381

dbfilename dump6381.rdb

appendfilename "appendonly6381.aof"

slaveof localhost 6379

编辑完成后,保存退出即可。然后,启动这三个Redis实例,依次运行命令bin/redis-server conf/redis6379.conf、bin/redis-serverconf/redis6380.conf、bin/redis-server conf/redis6381.conf,并分别启动三个命令行客户端连接到相应的Redis实例上,依次运行命令bin/redis-cli、bin/redis-cli -p 6380、bin/redis-cli -p 6381,最后,在主节点上插入一条字符串数据,并在三个Redis客户端进行读取,效果如下图所示,至此,Redis集群搭建成功。

5**、Redis的Sentinel(哨兵,实现HA)**

Sentinel(哨兵)是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器下属的所有从服务器,并在被监视的主服务器进入下线状态时,自动将主服务器下属的某个从服务器升级为新的主服务器。

哨兵(sentinel)虽然有一个单独的可执行文件redis-sentinel ,但实际上它只是一个运行在特殊模式下的Redis服务器,你可以在启动一个普通Redis服务器时通过给定--sentinel选项来启动哨兵(sentinel),哨兵(sentinel)的一些设计思路和Zookeeper非常类似。

Sentinel集群之间会互相通信,沟通交流Redis节点的状态,做出相应的判断并进行处理,这里的主观下线状态和客观下线状态是比较重要的状态,它们决定了是否进行故障转移,可以通过订阅指定的频道消息,当服务器出现故障的时候通知管理员,客户端可以将Sentinel看作是一个只提供了订阅功能的 Redis服务器,你不可以使用publish命令向这个服务器发送信息,但你可以用 subscribe命令或者psubscribe命令, 通过订阅给定的频道来获取相应的事件提醒。一个频道能够接收和这个频道的名字相同的事件,比如说,名为+sdown的频道就可以接收所有实例进入主观下线(SDOWN)状态的事件。

Sentinel(哨兵)进程的作用:

n监控(Monitoring):哨兵(sentinel)会不断地检查你的Master和Slave是否运行正常;

n提醒(Notification):当被监控的某个Redis节点出现问题时,哨兵(sentinel)可以通过API向管理员或者其他应用程序发送通知;

n自动故障迁移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel)会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master,并让失效Master的其他Slave改为复制新的Master;当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用现在的Master替换失效的Master。Master和Slave服务器切换后,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置文件的内容都会发生相应的改变,sentinel.conf的监控目标也会随之调换。

Sentinel(哨兵)进程的工作方式:

(1)每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个PING命令;

(2)如果一个实例(instance)距离最后一次有效回复PING命令的时间超过 down-after-milliseconds 选项所指定的值,则这个实例会被Sentinel(哨兵)进程标记为主观下线(SDOWN);

(3)如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态;

(4)当有足够数量的Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN),则Master主服务器会被标记为客观下线(ODOWN);

(5)在一般情况下,每个Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送INFO命令;

(6)当Master主服务器被Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的Master主服务器的所有Slave从服务器发送INFO 命令的频率会从10秒一次改为每秒一次;

(7)若没有足够数量的Sentinel(哨兵)进程同意Master主服务器下线, Master主服务器的客观下线状态就会被移除。若Master主服务器重新向Sentinel(哨兵)进程发送PING命令并返回有效回复,Master主服务器的主观下线状态也会被移除。

接下来,在前面搭建的Redis集群的基础上,再来配置Sentinel,以实现Redis集群的HA。Sentinel的核心配置文件是sentinel.conf,在安装目录下并没有该文件,需要从源码目录下进行拷贝,进入到/root/training/redis目录下,运行命令cp /root/training/redis-3.0.5/sentinel.conf conf/,拷贝一份sentinel.conf文件到conf/目录下。该文件中有几个参数比较重要,这里简单介绍下它们的作用,前面加了/*的参数,这里需要进行修改:

port 26379 ——>端口号

sentinel monitor <master-name> <ip><redis-port> <quorum>---> 配置哨兵监视对象的IP地址和端口号,及哨兵数目,master-name:主节点别名,ip:主节点IP地址,redis-port:主节点端口号,quorum:哨兵数目

(/*)sentinel monitormymaster 127.0.0.1 6379 1

sentinel auth-pass <master-name> <password> ——>如果主节点配置了密码,这里配置哨兵连接主节点的密码

sentinel down-after-milliseconds <master-name><milliseconds>

(/*)sentineldown-after-milliseconds mymaster 30000 ——>如果30秒内没有收到主节点的心跳,进行HA的切换

(/*)sentinelparallel-syncs mymaster 1 ——>选举新的主节点后,允许同时接连的从节点个数,这个参数一定不能太大,否则会给主节点造成太大的压力

sentinel failover-timeout mymaster 180000 ——>如果3分钟内,HA的切换没有完成,就失败

修改完成后,保存退出。进入到/root/training/redis目录下,运行命令bin/redis-sentinel conf/sentinel.conf,可以看到如下图所示的log信息,表明Sentinel启动成功。

当使用命令kill -9杀掉端口号为6379的Redis主节点后,可以看到如下图所示的log信息。从log信息中可以看出,Sentinel哨兵监控到主节点6379宕机,并将节点6381选举为了主节点。

6**、Redis的分片代理**

分片(partitioning)就是将你的数据拆分到多个Redis实例的过程,这样每个实例将只包含所有键的子集。Redis的分片承担着两个主要目标:

n允许使用很多电脑的内存总和来支持更大的缓存,没有分片,你就被局限于单机能支持的内存容量。

n允许伸缩计算能力到多核或多服务器,伸缩网络带宽到多服务器或多网络适配器。

Twemproxy又称nutcracker,是一个memcache、Redis协议的轻量级代理,一个用于sharding(碎片,这里是分散的意思)的中间件。有了Twemproxy,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。Twemproxy为Twitter开源产品,简单来说,Twemproxy是Twitter开发的一个redis代理proxy,类似于nginx的反向代理或者mysql的代理工具,如amoeba。Twemproxy通过引入一个代理层,可以将其后端的多台Redis或Memcached实例进行统一管理与分配,使应用程序只需要在Twemproxy上进行操作,而不用关心后面具体有多少个真实的Redis或Memcached存储。

使用Nutcracker实现Redis的代理分片,其架构如下图所示:

接下来就在前面Redis集群的基础之上,安装nutcracker,实现Redis集群的代理分片。

将nutcracker的安装包上传到/root/tools目录下,并进入到/root/tools目录下,运行命令mkdir /root/training/proxy,创建proxy目录,运行命令 tar -zxvf nutcracker-0.3.0.tar.gz,将nutcracker安装包解压到当前目录下,进入到/root/tools/nutcracker-0.3.0目录下,运行命令./configure--prefix=/root/training/proxy,配置nutcracker的安装目录为/root/training/proxy,运行命令make进行编译,运行命令make install进行安装,进入到/root/training/proxy目录下,运行命令mkdir conf,创建conf文件夹,用于存放nutcracker的配置文件,运行命令cp /root/tools/nutcracker-0.3.0/conf/nutcracker.yml conf/,从nutcracker源码目录下拷贝一份配置文件到conf目录下。进入到conf目录下,编辑nutcracker.yml文件,保留alpha这一段代码(nutcracker是一个通用的分片代理工具,因此该文件中提供了很多其他的默认配置,这里用不到,直接删除即可),并修改servers的内容如下,注意最后的1表示权重,数值越大其代理的请求就会越多,负载也就越大。

  • 192.168.12.221:6380:1

  • 192.168.12.221:6381:1

保存退出。下面验证配置文件是否配置正确,运行命令sbin/nutcracker-t conf/nutcracker.yml,如果输出log为nutcracker:configuration file 'conf/nutcracker.yml' syntax is ok,则表明nutcracker配置正确。运行命令sbin/nutcracker -d -cconf/nutcracker.yml,启动nutcracker。先运行命令bin/redis-cli登录到Redis主节点上,运行命令set key helloworld,插入一条数据,然后新打开一个窗口,运行命令bin/redis-cli-p 22121,登录到nutcracker端口上,依次执行插入数据和查询数据的命令,运行结果如下图所示。可见,nutcracker只是为Redis集群中的从节点实行代理,而从节点默认是只读不可写,这样便很好地实现了读写分离,减轻主节点的压力。

7**、Redis Cluster**

Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。

A**、数据分布理论与Redis的数据分区**

分布式数据库首先要解决的就是,把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据集的一个子集。常见的分区规则有哈希分区和顺序分区。Redis Cluster采用哈希分区规则。

虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot),比如Redis Cluster槽的范围是0~16383,槽是集群内数据管理和迁移的基本单位。

RedisCluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383,计算公式:slot =CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。

B**、RedisCluster的体系结构**

这里以6个节点为例,介绍Redis Cluster的体系结构,其中,三个节点为master,另外三个节点为slave。这6个节点构成的Redis Cluster的体系结构如下图所示:

C**、手动安装和部署****Redis Cluster**

Redis的编译和安装

前面已经介绍过Redis的编译和安装,这里的安装过程完全一致,不再介绍。先前已经在主机hadoop221上安装了Redis,现在在这个基础上来安装和部署RedisCluster。

安装Ruby环境和Ruby Redis接口

由于创建和管理Redis Cluster需要使用到redis-trib工具,该工具位于Redis源码的src文件夹下,它是一个Ruby程序,这个程序通过向实例发送特殊命令来构建新集群,检查集群,或者对集群进行重新分片(reshared)等工作,所以需要安装Ruby环境和相应的Redis接口。

挂载光盘,运行命令mount /dev/cdrom /mnt(确保物理光盘已经连接上),配置yum源(这里是通过yum源进行安装Ruby),进入到/etc/yum.repos.d目录下,删除原来存在的所有.repo文件,运行命令vi my.repo,在文件中添加如下内容:

[media]

name=Red Hat Enterprise Linux 7.4

baseurl=file:///mnt

enabled=1

gpgcheck=1

gpgkey=file:///mnt/RPM-GPG-KEY-redhat-release

保存退出,运行命令yum install ruby,在安装的过程中有三个地方需要输入y并按回车确认。然后,安装Ruby访问Redis的接口,将redis-3.0.5.gem工具上传到/root/tools目录下,进入到/root/tools目录下,运行命令gem installredis-3.0.5.gem即可。

安装部署Redis Cluster

这里以6个节点为例,安装和部署Redis Cluster,主节点端口号分别为:6379,6380,6381,从节点分别为:6382,6383,6384。为了不与先前Redis的配置相冲突,进入到/root/training/redis目录下,运行命令mkdir clusterconf,创建一个clusterconf文件夹专门用于存放Redis Cluster的配置文件。拷贝6份redis.conf文件到clusterconf目录下,分别命名为redis6379.conf~redis6384.conf,需要修改的参数如下,其中红色字体部分需要每个配置文件单独修改,其他部分每个配置文件保持一致即可。下面cluster-config-file这个参数需要解释下,Redis Cluster的配置信息会自动生成并存放到nodes/目录下,其每个Redis实例都会保存一份Cluster的配置信息,以确保某个节点宕机后,其他节点无法读取完整的Cluster配置信息。

daemonize yes

port 6379

cluster-enabled yes

cluster-config-file nodes/nodes-6379.conf

cluster-node-timeout 15000

dbfilename dump6379.rdb

appendonly yes

appendfilename "appendonly6379.aof"

启动Redis Cluster

进入到/root/training/redis目录下,依次运行如下命令:

bin/redis-server clusterconf/redis6379.conf

bin/redis-server clusterconf/redis6380.conf

bin/redis-server clusterconf /redis6381.conf

bin/redis-server clusterconf /redis6382.conf

bin/redis-server clusterconf /redis6383.conf

bin/redis-server clusterconf /redis6384.conf

通过ps命令可以查看Redis进程启动的情况,如下图所示:

到目前为止,仅仅启动了6个Redis实例,它们都是平等的,并没有主节点和从节点之分,需要通过redis-trib.rb命令脚步来自动创建Redis Cluster。redis-trib.rb命令脚步默认没有,需要从redis源码目录下拷贝,运行命令cp /root/training/redis-3.0.5/src/redis-trib.rb bin/,这样bin目录下就有redis-trib.rb命令工具了。运行命令bin/redis-trib.rb create --replicas1 192.168.12.221:6379 192.168.12.221:6380 192.168.12.221:6381192.168.12.221:6382 192.168.12.221:6383 192.168.12.221:6384,输出的log信息如下图所示,可以看到该命令脚本自动将6379,6380,6381创建为主节点,将6382,6383,6384创建为从节点。然后,输入yes并按回车键,等待Redis Cluster创建完成。

运行命令bin/redis-cli -c -p 6379,连接到Redis Cluster的主节点6379上,分别插入几条数据,结果如下图所示。可以看到,Redis会自动计算插入key的hash值,进而路由到相应的Redis节点,并将value值保存到该节点的slot中。

运行命令ps -ef | grep redis,查看6379的进程号,再运行命令kill -9进程号杀掉6379节点的Redis实例,最后运行命令bin/redis-trib.rb check192.168.12.221:6380,查看Redis Cluster的状态,输出的log信息如下图所示。可以看到,Redis Cluster自动将6382节点的Redis实例升级为了主节点,实现了Redis Cluster的HA。

D**、自动部署****RedisCluster**

在使用create-cluster脚本自动部署Redis Cluster之前,先将运行的所有Redis实例停止掉。该脚本在Redis源码的utils目录下,进入到/root/training/redis目录下,运行命令cp /root/training/redis-3.0.5/utils/create-cluster/create-clusterbin/,这样在bin/目录下就有create-cluster命令脚本了。运行命令vi bin/create-cluster,将所有的../../src/路径修改为/root/training/redis/bin/(一共有4处,对应start,create,stop,watch四处),修改完后,保存退出。运行命令bin/create-cluster start,创建6个Redis实例,再运行bin/create-cluster start,构建Redis Cluster,如下图所示。最后,输入yes并按回车键,大功告成,由此可见,自动部署真是轻松无比。

8**、Redis经典案例**

A**、节衣缩食——位图**

在平时的软件开发过程中,会有一些boolean类型的数据需要存取,比如用户一年的签到记录,签了是1,没签是0,要记录365天。如果使用普通的key/value,每个用户要记录365个,当用户上亿的时候,需要的存储空间是惊人的。为了解决这个问题,Redis提供了位图数据结构,这样用户每天的签到记录只占据一个位,365天就是365个位,46个字节(一个稍长一点的字符串)就可以完全容纳下,这就大大节约了存储空间。位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是byte数组。我们可以使用普通的get/set直接获取和设置整个位图的内容,也可以使用位图操作getbit/setbit等将byte数组看成「位数组」来进行处理。

基本使用

Redis的位数组是自动扩展的,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。接下来使用位操作将字符串设置为hello(不是直接使用set指令)。hello的ASCII码依次为0b1101000,0b1100101,0b1101100,0b1101100,0b1101111。接下来使用redis-cli命令行设置第一个字符,也就是位数组的前8位,只需要设置值为1的位即可,h字符只有1/2/4位需要设置,e字符只有9/10/13/15位需要设置。值得注意的是位数组的顺序和字符的位顺序是相反的,如下图所示。

上面这个例子可以理解为「零存整取」,同样也可以「零存零取」,「整存零取」。「零存」就是使用setbit对位值逐个进行设置,「整存」就是使用字符串一次性填充所有位数组,覆盖掉旧值。如果对应的字节是不可打印字符,redis-cli会显示该字符的16进制形式。整存零取举例如下图:

统计和查找

Redis提供了位图统计指令bitcount和位图查找指令bitpos,bitcount用来统计指定位置范围内1的个数,bitpos用来查找指定范围内出现的第一个0或1。比如可以通过bitcount统计用户一共签到了多少天,通过bitpos指令查找用户从哪一天开始第一次签到。如果指定了范围参数[start, end],就可以统计在某个时间范围内用户签到了多少天,用户自某天以后的哪天开始签到。

可惜的是,start和end参数是字节索引,也就是说指定的位范围必须是8的倍数,而不能任意指定。这很奇怪,表示不是很能理解Redis的作者为什么要这样设计。因为这个设计,无法直接计算某个月内用户签到了多少天,而必须要将这个月所覆盖的字节内容全部取出来(getrange可以取出字符串的子串)然后在内存里进行统计,这个就显得非常繁琐看了。Bitcout指令和bitpos指令的使用,举例如下图所示:

魔术指令****bitfield

前面设置(setbit)和获取(getbit)指定位的值都是单个位的,如果要一次操作多个位,就必须使用管道来处理。不过Redis 3.2版本以后新增了一个功能强大的指令,有了这条指令,不用管道也可以一次进行多个位的操作。bitfield有三个子指令,分别是get/set/incrby,它们都可以对指定位片段进行读写,但是最多只能处理64个连续的位,如果超过64位,就得使用多个子指令,bitfield可以一次执行多个子指令。举例如下图所示,

所谓有符号数是指获取的位数组中第一个位是符号位,剩下的才是值。如果第一位是1,那就是负数。无符号数表示非负数,没有符号位,获取的位数组全部都是值。有符号数最多可以获取64位,无符号数只能获取63位(因为Redis协议中的integer是有符号数,最大64位,不能传递64位无符号值)。如果超出位数限制,Redis就会告诉你参数错误。接下来还可以一次执行多个子指令,如下图所示:

然后使用set子指令将第二个字符e改成a,a的ASCII码是97,如下图所示:

再来看看子指令incrby,它用来对指定范围的位进行自增操作。既然提到自增,就有可能出现溢出。如果增加的是正数,会出现上溢,如果增加的是负数,就会出现下溢出。Redis默认的处理是折返。如果出现了溢出,就将溢出的符号位丢掉。如果是8位无符号数255,加1后就会溢出,会全部变零。如果是8位有符号数127,加1后就会溢出变成-128。举例如下图所示:

bitfield指令提供了溢出策略子指令overflow,用户可以选择溢出行为,默认是折返(wrap),还可以选择失败(fail)报错不执行,以及饱和截断(sat),超过了范围就停留在最大最小值。overflow指令只影响接下来的第一条指令,这条指令执行完后溢出策略会变成默认值折返(wrap)。饱和截断SAT举例如下图所示:

失败不执行FAIL举例如下图所示:

B**、大海捞针——**scan

在平时线上Redis的维护工作中,常常需要从Redis实例成千上万的key中找出特定前缀的key列表来手动处理数据,可能是修改它的值,也可能是删除key。这里就有一个问题,如何从海量的key中找出满足特定前缀的key列表来?Redis提供了一个简单粗暴的指令keys,用来列出所有满足特定正则字符串规则的key。举例如下图所示:

这个指令的使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点:1、没有offset、limit参数,一次性吐出所有满足条件的key,万一实例中有几百万个key满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了;2、keys算法是遍历算法,复杂度是O(n),如果实例中有千万级以上的key,这个指令就会导致Redis服务卡顿,所有读写Redis的其它指令都会被延后甚至会超时报错,因为Redis是单线程的,顺序执行所有指令,其它指令必须等到当前的keys指令执行完了才可以继续执行。

面对这两个显著的缺点该怎么办呢?Redis为了解决这个问题,在2.8版本中加入了大海捞针的指令——scan。scan相比keys具备以下特点:

复杂度虽然也是O(n),但是它是通过游标分步进行的,不会阻塞线程;
*
提供limit参数,可以控制每次返回结果的最大条数,limit只是一个hint,返回的结果可多可少;
*
同keys一样,它也提供模式匹配功能;
*
服务器不需要为游标保存状态,游标的唯一状态就是scan返回给客户端的游标整数;
*
返回的结果可能会有重复,需要客户端去重,这点非常关键;
*
遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
*
单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零。

scan****基础使用

使用Java程序往Redis里插入10000条数据来进行测试,程序代码如下图所示:

现在,Redis中已经有了10000条数据,接下来找出以key99开头的key列表。scan命令提供了三个参数,第一个是cursor整数值,第二个是key的正则模式,第三个是遍历的limit hint。第一次遍历时,cursor的值为0,然后将返回结果中第一个整数值作为下一次遍历的cursor,一直遍历到返回的cursor值为0时结束。运行结果如下图所示:

从上面的运行结果可以看到,虽然提供的limit是1000,但是返回的结果只有10个左右。因为这个limit不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。如果将limit设置为10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结束。如下图所示:

字典的结构

在Redis中,所有的key都存储在一个很大的字典中,这个字典的结构和Java中的HashMap一样,是一维数组+二维链表结构,第一维数组的大小总是2^n(n>=0),扩容一次数组大小空间加倍,也就是n++。如下图所示:

scan指令返回的游标就是第一维数组的位置索引,将这个位置索引称为槽(slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将limit数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

scan****遍历顺序

scan的遍历顺序非常特别,它不是从第一维数组的第0位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位遍历的重复和遗漏。高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位并且没有重复。

字典扩容

Java中的HashMap有扩容的概念,当loadFactor达到阈值时,需要重新分配一个新的2倍大小的数组,然后将所有的元素全部rehash挂到新的数组下面。rehash就是将元素的hash值对数组长度进行取模运算,因为长度变了,所以每个元素挂接的槽位可能也发生了变化。又因为数组的长度是2^n次方,所以取模运算等价于位与操作。

a mod 8 = a & (8-1) = a & 7

a mod 16 = a & (16-1) = a & 15

a mod 32 = a & (32-1) = a & 31

这里的7,,15,,31称之为字典的mask值,mask值的作用就是保留hash值的低位,高位都被设置为0。接下来看看rehash前后元素槽位的变化。假设当前字典的数组长度由8位扩容到16位,那么3号槽位011将会被rehash到3号槽位和11号槽位,也就是说该槽位链表中大约有一半的元素还是3号槽位,其它的元素会放到11号槽位,11这个数字的二进制是1011,就是对3的二进制011增加了一个高位1。如下图所示:

抽象一点说,假设开始槽位的二进制数是xxx,那么该槽位中的元素将被rehash到0xxx和1xxx(xxx+8)中。如果字典长度由16位扩容到32位,那么对于二进制槽位xxxx中的元素将被rehash到0xxxx和1xxxx(xxxx+16)中。

渐进式****rehash

Java的HashMap在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如果HashMap中元素特别多,线程就会出现卡顿现象。Redis为了解决这个问题,采用渐进式rehash。它会同时保留旧数组和新数组,然后在定时任务中以及后续对hash的指令操作中,渐渐地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于rehash中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。scan也需要考虑这个问题,对与rehash中的字典,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。

更多的scan指令

scan指令是一系列指令,除了可以遍历所有的key之外,还可以对指定的容器集合进行遍历。比如zscan遍历zset集合元素,hscan遍历hash字典的元素sscan遍历set集合的元素。

它们的原理同scan都是类似的,因为hash底层就是字典,set也是一个特殊的hash(所有的value指向同一个元素),zset内部也使用了字典来存储所有的元素内容,所以这里就不再赘述了。

C**、更多关于Redis的经典应用**

关于Redis的经典应用,除了上面介绍的位图和scan指令,还有很多其他的经典应用,比如:千帆竞发——分布式锁、缓兵之计——延迟队列、四两拨千斤——HyperLogLog、层峦叠嶂——布隆过滤器、断尾求生——简单限流、一毛不拔——漏斗限流、近水楼台——GeoHash等等。Redis的这些特性(或者说经典应用)也许平时不一定用得到,但说不准在某些应用场景下就能够发挥巨大的价值,因此在平时的学习中有必要进行了解。由于篇幅受限,这里就不再对这些Redis的经典应用进行介绍,有兴趣的朋友可以上网找资料进一步深入学习。

参考文献:

——《Redis深度历险:核心原理与应用实践》

——《CSDN博客》

——《潭州大数据课程课件》

转自https://mp.weixin.qq.com/s/TMV5H0Ui5dVeoFAb67MuBw

相关文章

热门文章

更多