文章目录

  • 1.redis架构基本搭建与原理
  • 1.1.主从模式
  • 1.1.1.主从架构搭建
  • 1.1.2.主从从架构搭建
  • 1.1.3.主从复制原理
  • 1.1.4.一些调用细节和注意事项
  • 1.2.哨兵模式
  • 1.2.1.哨兵架构搭建
  • 1.2.2.哨兵模式常用命令与配置
  • 1.2.3.哨兵模式原理
  • 1.2.4.注意事项
  • 1.3.集群模式
  • 1.3.1.Redis Cluster环境搭建
  • 1.3.2.新增集群节点
  • 1.3.3.删除集群节点
  • 1.3.4.集群模式高可用
  • 1.3.5.集群模式原理
  • 2.springboot2.0调用redis高可用架构
  • 2.1.依赖
  • 2.2.application.properties配置
  • 2.3.封装调用


1.redis架构基本搭建与原理

1.1.主从模式

Redis的主从结构主要提供了以下功用

1、构建读写分离架构,满足读多写少的应用场景

2、可以通过手动数据库转移来避免Redis单点故障

架构图

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器

1.1.1.主从架构搭建

基于redis单机版本(这里建议版本为Redis3.0+,因为下文哨兵模式的部分配置基于Redis2.8+,集群模式基于Redis3.0+)
(1)在安装好单机版的前提下,复制三份配置文件

# cd /root/2019dev/redis-4.0.1

创建6379、6380、6381目录,分别将安装目录下的redis.conf拷贝到这三个目录下

# mkdir -p /root/2019dev/redis-4.0.1/6379 && cp redis.conf /root/2019dev/redis-4.0.1/6379/6379.conf
# mkdir -p /root/2019dev/redis-4.0.1/redis/6380 && cp redis.conf /root/2019dev/redis-4.0.1/6380/6380.conf
# mkdir -p /root/2019dev/redis-4.0.1/6381 && cp redis.conf /root/2019dev/redis-4.0.1/6381/6381.conf

(2)修改配置文件

# vim /root/2019dev/redis-4.0.1/6379/6379.conf 

#使用后台模式
daemonize yes

# 关闭保护模式
protected-mode no

# 注释以下内容开启远程访问
# bind 127.0.0.1

# 修改启动端口为6379
port 6379

# 修改pidfile指向路径
pidfile /root/2019dev/redis-4.0.1/6379/redis_6379.pid

#设置log文件路径(需要创建logs目录)
logfile "/root/2019dev/redis-2.8.17/logs/6379/redis.log"

#设置dir路径,让rdb、so文件生成到指定路径,否则这些文件将在redis启动目录开启
dir /root/2019dev/redis-2.8.17/6379/

#密码配置(根据需求)
requirepass "root"

这里注意需要把.conf文件配置授权(避免提示Failed opening .rdb for saving: Permission denied

chmod 755 6379.conf

以此类推,修改6380.conf、6381.conf
注意,如果主服务设置有密码,slave需要设置masterauth

masterauth "root"

(3)启动三个Redis实例

# cd /root/2019dev/redis-4.0.1/src/
# ./redis-server ../6379/6379.conf 
# ./redis-server ../6380/6380.conf && ./redis-server ../6381/6381.conf

(4)设置主从
在Redis中设置主从有2种方式:
1.在redis.conf中设置slaveof

slaveof <masterip> <masterport>

2.使用redis-cli客户端连接到redis服务,执行slaveof命令

slaveof <masterip> <masterport>

(该方式在重启后将失去主从复制关系)
我们这里使用第二种方式设置主从:
使用Redis客户端连接上6380端口

xx.xx.xx.xx对应自己服务器的ip地址

# ./redis-cli -h xx.xx.xx.xx -p 6380 -a root
设置6380端口Redis为6379的从
xx.xx.xx.xx:6380> slaveof xx.xx.xx.xx 6379
OK

使用Redis客户端连接上6381端口
# redis-cli -h xx.xx.xx.xx -p 6381
设置6381端口Redis为6379的从
xx.xx.xx.xx:6381> slaveof xx.xx.xx.xx 6379
OK

3.查看Redis主从关系

使用Redis客户端连接上6379端口
# ./redis-cli -h xx.xx.xx.xx -p 6379 -a root

查看Redis主从关系 如下图所示
xx.xx.xx.xx:6379> info replication

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_spring boot_02

role:角色信息 
slaveX:从库信息 
connected_slaves:从库数量

也可以访问从服务查询服务连接情况info replication

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_03


(6)测试

在主库写入数据

xx.xx.xx.xx:6379> set master "abc"
OK

在从库读取数据

xx.xx.xx.xx:6380> get master
"abc"

1.1.2.主从从架构搭建


springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_04

主从架构的缺点是所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,我们可以使用主从从结构来处理

(1)搭建

搭建步骤与1.1.1.相同,只是在设置主从结构时,设置6380为6379的从,6381为6380的从

主从信息如下:

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_05


springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_06


springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_07


(2)测试

xx.xx.xx.xx:6379> set master "555"
OK
xx.xx.xx.xx:6380> get master
"555"
xx.xx.xx.xx:6381> get master
"555"

1.1.3.主从复制原理

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_08


过程:

1:当一个从数据库启动时,会向主数据库发送sync命令,

2:主数据库接收到sync命令后会开始在后台保存快照(执行rdb操作),并将保存期间接收到的命令缓存起来

3:当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库。

4:从数据库收到后,会载入快照文件并执行收到的缓存的命令。

注意:redis2.8之前的版本:当主从数据库同步的时候从数据库因为网络原因断开重连后会重新执行上述操作,不支持断点续传。redis2.8之后支持断点续传。

1.1.4.一些调用细节和注意事项

(1)让从库可写
默认情况下redis数据库充当slave角色时是只读的不能进行写操作

可以在配置文件中开启非只读:slave-read-only no

(2)无磁盘复制
通过前面的复制过程我们了解到,主库接收到SYNC的命令时会执行RDB过程,即使在配置文件中禁用RDB持久化也会生成,那么如果主库所在的服务器磁盘IO性能较差,那么这个复制过程就会出现瓶颈,庆幸的是,Redis在2.8.18版本开始实现了无磁盘复制功能(不过该功能并不稳定)。

原理:Redis在与从数据库进行复制初始化时将不会将快照存储到磁盘,而是直接通过网络发送给从数据库,避免了IO性能差问题。

开启无磁盘复制:repl-diskless-sync yes

(3)取消主从关系
如果要取消Redis主从关系,可以在对应的从库执行SLAVEOF NO ONE命令,取消主从关系
(4)注意事项
如果你使用主从复制,那么要确保你的master激活了持久化,或者确保它不会在当掉后自动重启。原因:
slave是master的完整备份,因此如果master通过一个空数据集重启,slave也会被清掉。
在配置redis复制功能的时候如果主数据库设置了密码,需要在从数据的配置文件中通过masterauth参数设置主数据库的密码,这样从数据库在连接主数据库时就会自动使用auth命令认证了。相当于做了一个免密码登录。

1.2.哨兵模式

(1)为什么要用到哨兵
实现redis的高可用,使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性

哨兵(Sentinel)主要是为了解决在主从复制架构中出现宕机的情况,主要分为两种情况:

1.从Redis宕机 这个相对而言比较简单,在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据。Redis2.8版本后,主从断线后恢复的情况下实现增量复制。

2.主Redis宕机 这个相对而言就会复杂一些,需要以下2步才能完成 i.第一步,在从数据库中执行SLAVEOF NO ONE命令,断开主从关系并且提升为主库继续服务
ii.第二步,将主库重新启动后,执行SLAVEOF命令,将其设置为其他库的从库,这时数据就能更新回来

由于这个手动完成恢复的过程其实是比较麻烦的并且容易出错,所以Redis提供的哨兵(sentinel)的功能来解决

(2)什么是哨兵

redis的sentinel系统用于管理多个redis服务器,该系统主要执行三个任务:监控、提醒、自动故障转移。
1、监控(Monitoring): Redis Sentinel实时监控主服务器和从服务器运行状态,并且实现自动切换。
2、提醒(Notification):当被监控的某个 Redis 服务器出现问题时, Redis Sentinel
可以向系统管理员发送通知, 也可以通过 API 向其他程序发送通知。 3、自动故障转移(Automatic failover):
当一个主服务器不能正常工作时,Redis Sentinel 可以将一个从服务器升级为主服务器,
并对其他从服务器进行配置,让它们使用新的主服务器。当应用程序连接Redis 服务器时, Redis
Sentinel会告之新的主服务器地址和端口。
注意:在使用sentinel监控主从节点的时候,从节点需要是使用动态方式配置的,如果直接修改配置文件,后期sentinel实现故障转移的时候会出问题。

流程:

1.用户链接时先通过哨兵获取主机Master的信息

2.获取Master的链接后实现redis的操作(set/get)

3.当master出现宕机时,超过半数的哨兵通过心跳检测发现主机长时间没有响应.这时哨兵会进行推选.推选出新的主机完成任务(它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器).

4.当新的主机出现时,其余的全部机器都充当该主机的从机

(3)Sentinel拓扑图

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_09


springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_10


多个哨兵,不仅同时监控主从数据库,而且哨兵之间互为监控

1.2.1.哨兵架构搭建

(1)在Redis主从架构集群可用的前提下,复制三份配置文件

进入redis所在目录
# cd /root/2019dev/redis-4.0.1

创建6379、6380、6381目录,分别将安装目录下的sentinel.conf拷贝到这三个目录下
# mkdir -p /root/2019dev/redis-4.0.1/6379 && cp sentinel.conf /root/2019dev/redis-4.0.1/6379/26379.conf
# mkdir -p /root/2019dev/redis-4.0.1/6380 && cp sentinel.conf /root/2019dev/redis-4.0.1/6380/26380.conf
# mkdir -p /root/2019dev/redis-4.0.1/6381 && cp sentinel.conf /root/2019dev/redis-4.0.1/6381/26381.conf

(2)分别配置哨兵
注意:redis版本需要是Redis2.8+

修改sentinel配置文件
vim /root/2019dev/redis-4.0.1/6379/26379.conf

修改内容:
# 添加守护进程模式
daemonize yes

# 添加指明日志文件名
logfile "/root/2019dev/redis-4.0.1/6379/sentinel26379.log"

# 修改工作目录
dir "/root/2019dev/redis-4.0.1/6379"

# 修改启动端口
port 26379

# 关闭保护模式
protected-mode no

# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,xx.xx.xx.xx代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor redis-test-master xx.xx.xx.xx 6379 2

# 将配置文件中mymaster全部替换redis-test-master
#判断主master的挂机时间(毫秒),超时未返回正确信息后标记为sdown状态
sentinel down-after-milliseconds redis-test-master 30000
#若sentinel在该配置值内未能完成failover操作(即故障时master/slave自动切换),则认为本次failover失败。
sentinel failover-timeout redis-test-master 180000

# sentinel auth-pass定义服务的密码,redis-test-master是服务名称,root是Redis服务器密码,主机宕机后会将从机设置为新的主机,为了避免密码问题,在哨兵模式最好把所有的缓存服务器密码设置成相同,
sentinel auth-pass redis-test-master root

依次修改26380,26381配置(监听的主服务ip端口是相同,注意文件的权限问题)
说明:
redis-test-master:监控主数据的名称,自定义即可,可以使用大小写字母和“.-_”符号
xx.xx.xx.xx:监控的主数据库的IP
6379:监控的主数据库的端口
2:最低通过票数
(3)启动哨兵进程

./redis-sentinel ../6379/26379.conf && ./redis-sentinel ../6380/26380.conf && ./redis-sentinel ../6381/26381.conf

查看26379.conf、26380.conf、26381.conf配置文件发现自动生成新的配置

# Generated by CONFIG REWRITE
sentinel known-slave redis-test-master xx.xx.xx.xx(外网地址) 6381
sentinel known-slave redis-test-master xx.xx.xx.xx(外网地址) 6380
sentinel known-sentinel redis-test-master xx.xx.xx.xx(内网地址) 26380 ec84fdfc7beaf570f5ebef6d4e43cc5f7a576c42
sentinel known-sentinel redis-test-master xx.xx.xx.xx(内网地址) 26381 dd3a88c4e9300d4c2fa4ef302437f2f27bd0029c
sentinel current-epoch 0

(注意:如果想直接复制用过的sentinel.conf一定要记得把自动生成的这些配置删掉,不然会导致新服务选举失败)
登陆26379测试

# ./redis-cli -h xx.xx.xx.xx -p 26379 -a root
xx.xx.xx.xx:26379> SENTINEL SLAVES redis-test-master

可以看到主服务器的所有从服务器,以及这些从服务器的当前状态

1)  1) "name"
    2) "xx.xx.xx.xx:6380"
    3) "ip"
    4) "xx.xx.xx.xx"
……
2)  1) "name"
    2) "xx.xx.xx.xx:6381"
    3) "ip"
    4) "xx.xx.xx.xx"
……

(4)测试Sentinel是否正常工作

查看集群信息

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_11


关闭端口为6379服务

./redis-cli -h 172.16.0.6 -p 6379 -a root shutdown

登陆6380查看新的集群架构

(可能不会马上生效,可以登陆哨兵服务查看集群信息来刷新命令)

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_12


重启6379服务查看集群架构

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_13


该过程是6379宕机->6381切换成Master->6379和6381切换为6381的SLAVE->6379重新启动->6379为6381的SLAVE

(5)查看Sentinel日志

sentinel26380.log

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_14


sentinel26381.log

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_15

1.2.2.哨兵模式常用命令与配置

常用命令

1.查看sentinel的基本状态信息
127.0.0.1:26379> INFO

2.列出所有被监视的主服务器,以及这些主服务器的当前状态 
127.0.0.1:26379> SENTINEL MASTERS redis-test-master

3.列出给定主服务器的所有从服务器,以及这些从服务器的当前状态 
127.0.0.1:26379> SENTINEL SLAVES redis-test-master

4.返回给定名字的主服务器的IP地址和端口号
127.0.0.1:26379> SENTINEL GET-MASTER-ADDR-BY-NAME redis-test-master

5.重置所有名字和给定模式pattern相匹配的主服务器,重置操作清除主服务器目前的所有状态,包括正在执行中的故障转移,并移除目
前已经发现和关联的,主服务器的所有从服务器和Sentinel
127.0.0.1:26379> SENTINEL RESET redis-test-master 

6.当主服务器失效时,在不询问其他Sentinel意见的情况下,强制开始一次自动故障迁移,但是它会给其他Sentinel发送一个最新的配
置,其他sentinel会根据这个配置进行更新
127.0.0.1:26379> SENTINEL FAILOVER redis-test-master

7.查看其它哨兵信息
127.0.0.1:26379> SENTINEL sentinels redis-test-master

常用配置

配置1:sentinel monitor {masterName} {masterIp} {masterPort} {quorum}
sentinel monitor是哨兵最核心的配置,在前文讲述部署哨兵节点时已说明,其中:masterName指定了主节点名称,masterIp和masterPort指定了主节点地址,quorum是判断主节点客观下线的哨兵数量阈值:当判定主节点下线的哨兵数量达到quorum时,对主节点进行客观下线。建议取值为哨兵数量的一半加1。
配置2:sentinel down-after-milliseconds {masterName} {time} sentinel
down-after-milliseconds与主观下线的判断有关:哨兵使用ping命令对其他节点进行心跳检测,如果其他节点超过down-after-milliseconds配置的时间没有回复,哨兵就会将其进行主观下线。该配置对主节点、从节点和哨兵节点的主观下线判定都有效。
down-after-milliseconds的默认值是30000,即30s;可以根据不同的网络环境和应用要求来调整:值越大,对主观下线的判定会越宽松,好处是误判的可能性小,坏处是故障发现和故障转移的时间变长,客户端等待的时间也会变长。例如,如果应用对可用性要求较高,则可以将值适当调小,当故障发生时尽快完成转移;如果网络环境相对较差,可以适当提高该阈值,避免频繁误判。
配置3:sentinel parallel - syncs {masterName} {number} sentinel
parallel-syncs与故障转移之后从节点的复制有关:它规定了每次向新的主节点发起复制操作的从节点个数。例如,假设主节点切换完成之后,有3个从节点要向新的主节点发起复制;如果parallel-syncs=1,则从节点会一个一个开始复制;如果parallel-syncs=3,则3个从节点会一起开始复制。
parallel-syncs取值越大,从节点完成复制的时间越快,但是对主节点的网络负载、硬盘负载造成的压力也越大;应根据实际情况设置。例如,如果主节点的负载较低,而从节点对服务可用的要求较高,可以适量增加parallel-syncs取值。parallel-syncs的默认值是1。
配置4:sentinel failover - timeout {masterName} {time} sentinel
failover-timeout与故障转移超时的判断有关,但是该参数不是用来判断整个故障转移阶段的超时,而是其几个子阶段的超时,例如如果主节点晋升从节点时间超过timeout,或从节点向新的主节点发起复制操作的时间(不包括复制数据的时间)超过timeout,都会导致故障转移超时失败。
failover-timeout的默认值是180000,即180s;如果超时,则下一次该值会变为原来的2倍。

1.2.3.哨兵模式原理

Sentinel模式中存在两个概念,即主观下线(SDOWN)和客观下线(ODOWN)
(1)主观下线状态:当一个sentinel认为一个redis服务连接不上的时候,会给这个服务打个标记为下线状态

(2)客观下线状态:当多个sentinel认为一个redids连接不上的时候,则认为这个redis服务确实下线了。这里的多个sentinel的个数可以在配置文件中设置

(3)主观下线(SDOWN)与客观下线(ODOWN)的转换过程:
i.每个sentinel实例在启动后,都会和已知的slaves/master以及其他sentinels建立TCP连接,并周期性发送PING(默认为1秒),在交互中,如果redis-server无法在”down-after-milliseconds”时间内响应或者响应错误信息,都会被认为此redis-server处于SDOWN状态.
ii.SDOWN的server为master,那么此时sentinel实例将会向其他sentinel间歇性(一秒)发送”is-master-down-by-addr ”指令并获取响应信息,如果足够多的sentinel实例检测到master处于SDOWN,那么此时当前sentinel实例标记master为ODOWN…其他sentinel实例做同样的交互操作.配置项”sentinel monitor ”,如果检测到master处于SDOWN状态的slave个数达到,那么此时此sentinel实例将会认为master处于ODOWN.
每个sentinel实例将会间歇性(10秒)向master和slaves发送”INFO”指令,如果master失效且没有新master选出时,每1秒发送一次”INFO”;”INFO”的主要目的就是获取并确认当前集群环境中slaves和master的存活情况.
经过上述过程后,所有的sentinel对master失效达成一致后,开始failover.
(需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作)

(4)选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。
监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。选举的具体过程这里不做详细描述,一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。

(5)故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为3个步骤:
1、在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。
2、更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。
3、将已经下线的主节点设置为新的主节点的从节点,当6379重新上线后,它会成为新的主节点的从节点。

(6)哨兵之间的心跳机制:在sentinel的配置文件中,都指定了port,此port就是sentinel实例侦听其他sentinel实例建立链接的端口.在集群稳定后,最终会每个sentinel实例之间都会建立一个tcp链接,此链接中发送”PING”以及类似于”is-master-down-by-addr”指令集,可用用来检测其他sentinel实例的有效性以及所监听主从机的信息交互.在sentinel之间建立连接之前,sentinel将先将配置文件中指定的主机建立连接.sentinel与master的连接中的通信主要是基于pub/sub来发布和接收信息,发布的信息包括当前sentinel实例的侦听端口.

1.2.4.注意事项

1、如果主从库在同一局域网环境下,从库配置的slaveof可以设置为内网
2、哨兵模式配置的主数据库需要配置为外网,因为外部程序通过哨兵访问主服务器最终还是要去访问主服务
3、在哨兵模式下,程序中不再直接访问主机,而是通过哨兵地址来实现主从读写分离

1.3.集群模式

(1)为什么使用集群模式

因为Redis主从复制架构每个数据库都要保存整个集群中的所有数据,容易形成木桶效应,所以Redis3.0之后的版本添加特性就是集群(Cluster)

redis集群是一个无中心的分布式Redis存储架构,可以在多个节点之间进行数据共享,解决了Redis高可用、可扩展等问题。redis集群提供了以下两个好处

1、将数据自动切分(split)到多个节点

2、当集群中的某一个节点故障时,redis还可以继续处理客户端的请求。

一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个数据都属于这16384个哈希槽中的一个。集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽。集群中的每一个节点负责处理一部分哈希槽。

集群中的主从复制

集群中的每个节点都有1个至N个复制品,其中一个为主节点,其余的为从节点,如果主节点下线了,集群就会把这个主节点的一个从节点设置为新的主节点,继续工作。这样集群就不会因为一个主节点的下线而无法正常工作。

(2)集群模式可能出现的一些问题

1、如果某一个主节点和他所有的从节点都下线的话,redis集群就会停止工作了。redis集群不保证数据的强一致性,在特定的情况下,redis集群会丢失已经被执行过的写命令

2、使用异步复制(asynchronous replication)是 Redis 集群可能会丢失写命令的其中一个原因,有时候由于网络原因,如果网络断开时间太长,redis集群就会启用新的主节点,之前发给主节点的数据就会丢失。

(3)架构说明

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_spring boot_16


架构细节:

(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.

(2)节点的fail是通过集群中超过半数的master节点检测失效时才生效.

(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可

(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->key(缓存服务<->hash环<->存储的key值)

1.3.1.Redis Cluster环境搭建

基于1.1.2.的配置环境
1.3.1.1. 分别修改配置文件,将端口分别设置为:6379、6380、6381,设置pidfile文件为不同的路径,允许集群模式,修改集群配置文件指向地址,并且开启远程访问
修改配置文件

# vim /root/2019dev/redis-4.0.1/6379/6379.conf

# 开启守护进程模式
daemonize yes

# 修改启动端口为6379
port 6379

# 修改pidfile指向路径
pidfile /opt/redis/6379/redis_6379.pid

# 开启允许集群 
cluster-enabled yes

# 修改集群配置文件指向路径
cluster-config-file nodes-6379.conf

# 注释一下内容开启远程访问
# bind 127.0.0.1

# 关闭保护模式
protected-mode no

#设置log文件路径
logfile "/root/2019dev/redis-4.0.1/logs/6379/redis.log"

#设置dir路径,让rdb、so文件生成到指定路径,否则这些文件将在redis启动目录被创建
dir /root/2019dev/redis-4.0.1/6379/

#密码配置(可选)
requirepass root
#如果缓存服务器配置有密码需开启该配置,其他服务的访问密码,由于密码写死所以要让所有集群服务密码都保持一致
# masterauth root

这里注意需要把.conf文件配置授权(避免提示Failed opening .rdb for saving: Permission denied)

chmod 755 redis.conf

以此类推,修改端口6380及6381配置。
1.3.1.2.分别启动redis实例

#cd /root/2019dev/redis-4.0.1/src/
# ./redis-server ../6379/6379.conf
# ./redis-server ../6380/6380.conf     
# ./redis-server ../6381/6381.conf

1.3.1.3.查看redis状态

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_17


说明redis已经是以集群方式启动了,但是redis之间关系还没确定下来

1.3.1.4 因为redis-trib.rb是由ruby语言编写的所以需要安装ruby环境

安装ruby环境
# yum -y install zlib ruby rubygems

自行上传redis-3.2.1.gem然后安装
# gem install -l redis-3.2.1.gem

1.3.1.5 建立集群Redis关系

首先,进入redis的安装包路径下
# cd /root/2019dev/redis-4.0.1/src/

执行命令:
# ./redis-trib.rb create --replicas 0 xx.xx.xx.xx:6379 xx.xx.xx.xx:6380 xx.xx.xx.xx:6381

说明:–replicas 0:指定了从数据的数量为0
注意:
(1)这里不能使用本地或者内网ip,否则在Jedis客户端使用时无法连接到
(2)如果redis服务设置有密码通过redis-trib.rb实现集群会报[ERR] Sorry, can’t connect to node xx.xx.xx.xx:6379,因为6379的redis.conf没找到密码配置
解决办法:vim /usr/local/rvm/gems/ruby-2.3.3/gems/redis-4.0.0/lib/redis/client.rb,然后修改passord(client.rb路径可以通过find命令查找:find / -name ‘client.rb’)

class Client
    DEFAULTS = {
      :url => lambda { ENV["REDIS_URL"] },
      :scheme => "redis",
      :host => "127.0.0.1",
      :port => 6379,
      :path => nil,
      :timeout => 5.0,
      :password => "root",
      :db => 0,
      :driver => nil,
      :id => nil,
      :tcp_keepalive => 0,
      :reconnect_attempts => 1,
      :inherit_socket => false
}

1.3.1.6如果出现如下异常

/usr/local/share/gems/gems/redis-3.2.1/lib/redis/client.rb:113:in `call': ERR Slot 0 is already busy (Redis::CommandError)
        from /usr/local/share/gems/gems/redis-3.2.1/lib/redis.rb:2556:in `block in method_missing'
        from /usr/local/share/gems/gems/redis-3.2.1/lib/redis.rb:37:in `block in synchronize'
        from /usr/share/ruby/monitor.rb:211:in `mon_synchronize'
        from /usr/local/share/gems/gems/redis-3.2.1/lib/redis.rb:37:in `synchronize'
        from /usr/local/share/gems/gems/redis-3.2.1/lib/redis.rb:2555:in `method_missing'
        from ./redis-trib.rb:212:in `flush_node_config'
        from ./redis-trib.rb:776:in `block in flush_nodes_config'
        from ./redis-trib.rb:775:in `each'
        from ./redis-trib.rb:775:in `flush_nodes_config'
        from ./redis-trib.rb:1296:in `create_cluster_cmd'
        from ./redis-trib.rb:1701:in `<main>'

或者

Node xx.xx.xx.xx:7001 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.

经检查,这是由于上一次配置集群失败时留下的配置信息导致的
解决方法:
1)将需要新增的节点下aof、rdb等本地备份文件删除;
2)同时将新Node的集群配置文件删除,即:删除你redis.conf里面cluster-config-file所在的文件;
3)再次添加新节点如果还是报错,则登录新Node,./redis-cli–h x –p对数据库进行清除:
xx.xx.xx.xx:7001> flushdb #清空当前数据库

通过以上解决方法中的1)、2)或者3)之后再执行脚本,成功执行;
1.3.1.7 建立集群Redis关系正常执行响应如下

>>> Creating cluster
>>> Performing hash slots allocation on 3 nodes...
Using 3 masters:
xx.xx.xx.xx:6379
xx.xx.xx.xx:6380
xx.xx.xx.xx:6381
M: d5d0951bb185a67a44d29dd2142170dbce84d977 xx.xx.xx.xx:6379
   slots:0-5460 (5461 slots) master
M: e41fe58ef571836d891656b482307628b3f7ab35 xx.xx.xx.xx:6380
   slots:5461-10922 (5462 slots) master
M: ddbc810661f81500059e0b22b1550713a0e3766d xx.xx.xx.xx:6381
   slots:10923-16383 (5461 slots) master
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join...
>>> Performing Cluster Check (using node xx.xx.xx.xx:6379)
M: d5d0951bb185a67a44d29dd2142170dbce84d977 xx.xx.xx.xx:6379
   slots:0-5460 (5461 slots) master
   0 additional replica(s)
M: ddbc810661f81500059e0b22b1550713a0e3766d xx.xx.xx.xx:6381
   slots:10923-16383 (5461 slots) master
   0 additional replica(s)
M: e41fe58ef571836d891656b482307628b3f7ab35 xx.xx.xx.xx:6380
   slots:5461-10922 (5462 slots) master
   0 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
成功

1.3.1.8 查看集群节点信息

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_18


1.3.1.9 测试

1.3.1.9.1 测试插入数据

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_19


因为abc的hash槽信息是在6380上,现在使用redis-cli连接的6379,无法完成set操作,需要客户端跟踪重定向。使用redis-cli -c

1.3.1.9.2 重新测试插入数据

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_20

1.3.2.新增集群节点

1.3.2.1 再开启一个实例的端口为6382 配置同上

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_21


1.3.2.2 执行脚本建立6382节点与集群的关系

./redis-trib.rb add-node xx.xx.xx.xx:6382 xx.xx.xx.xx:6379

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_22


1.3.2.3 查看集群状态,发现新增的节点没有插槽

cluster nodes

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_23


可以看到新增的node:6382没有对应的插槽值

1.3.2.4 给6382节点分配插槽

./redis-trib.rb reshard xx.xx.xx.xx:6379

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_24


redis-trib.rb命令可以到官方文档去了解

1.3.2.5 查看集群节点状态

cluster nodes

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_25

1.3.3.删除集群节点

想要删除集群节点中的某一个节点,需要严格执行2步:

1.3.3.1.将这个节点上的所有插槽转移到其他节点上

1.3.3.2执行脚本:./redis-trib.rb reshard xx.xx.xx.xx:6382

1.3.3.3选择需要转移的插槽的数量,因为6382有100个,所以转移100个(要根据cluster nodes查询出来具体的节点个数)

1.3.3.4输入转移的节点的id,我们转移到6379节点

1.3.3.5输入插槽来源id,也就是6382的id

1.3.3.6输入done,开始转移

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_26


异常:

在执行./redis-trib.rb reshard xx.xx.xx.xx:6382时提示异常

[WARNING] Node xx.xx.xx.xx:6382 has slots in importing state (135).

解决

登录到提示错误的两个结点,然后执行下面的清除操作

cluster setslot 135 stable

Ps:

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_27


springboot redistemplate可以使用redis集群吗 springboot调用redis集群_redis_28


1.3.3.7.查看集群节点信息,可以看到6382节点已经没有插槽了

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_29


1.3.3.8.删除节点

1.3.3.9. 删除节点

./redis-trib.rb del-node xx.xx.xx.xx:6382 f0742c2ecd32cda67c2c5ea7c4883024f49d645b

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_30


1.3.3.10. 查看集群节点信息

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_31

1.3.4.集群模式高可用

1.3.4.1 假设集群中某一节点宕机 测试数据写入操作

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_服务器_32


我们尝试执行set命令,结果发现无法执,行集群不可用了

此时需要考虑集群模式下的主从复制架构

1.3.4.2 集群中的主从复制架构

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_33


1.3.4.3 配置流程

(1)跟1.3.1.配置一致,配置6个config,启动完配置集群,设置主从节点比例为1

Ps:

./redis-trib.rb create --replicas 1 xx.xx.xx.xx:6379 xx.xx.xx.xx:6380 xx.xx.xx.xx:6381 xx.xx.xx.xx:6479 xx.xx.xx.xx:6480 xx.xx.xx.xx:6481
[root@VM_0_6_centos src]# ./redis-trib.rb create --replicas 1 xx.xx.xx.xx:6379 xx.xx.xx.xx:6380 xx.xx.xx.xx:6381 xx.xx.xx.xx:6479 xx.xx.xx.xx:6480 xx.xx.xx.xx:6481

>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
xx.xx.xx.xx:6379
xx.xx.xx.xx:6380
xx.xx.xx.xx:6381
Adding replica xx.xx.xx.xx:6479 to xx.xx.xx.xx:6379
Adding replica xx.xx.xx.xx:6480 to xx.xx.xx.xx:6380
Adding replica xx.xx.xx.xx:6481 to xx.xx.xx.xx:6381
M: 9454a62b681adcf4580276c39159f24cc3c227d6 xx.xx.xx.xx:6379
   slots:0-5460 (5461 slots) master
M: 604e6470d535310c29c7520a2f53109e416a9604 xx.xx.xx.xx:6380
   slots:5461-10922 (5462 slots) master
M: 8fde0b5a819e567f9911683b20ef04dc963d2697 xx.xx.xx.xx:6381
   slots:10923-16383 (5461 slots) master
S: 05a18c4227484f50de8d800546108a6b300ffea8 xx.xx.xx.xx:6479
   replicates 9454a62b681adcf4580276c39159f24cc3c227d6
S: 6452ec3bab699a6184af8e9f993ad97f24780d32 xx.xx.xx.xx:6480
   replicates 604e6470d535310c29c7520a2f53109e416a9604
S: c5bf71c1dec51c3446eca58fa0a4f74f5b94f647 xx.xx.xx.xx:6481
   replicates 8fde0b5a819e567f9911683b20ef04dc963d2697
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join....
>>> Performing Cluster Check (using node xx.xx.xx.xx:6379)
M: 9454a62b681adcf4580276c39159f24cc3c227d6 xx.xx.xx.xx:6379
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
M: 8fde0b5a819e567f9911683b20ef04dc963d2697 xx.xx.xx.xx:6381
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
M: 604e6470d535310c29c7520a2f53109e416a9604 xx.xx.xx.xx:6380
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 05a18c4227484f50de8d800546108a6b300ffea8 xx.xx.xx.xx:6479
   slots: (0 slots) slave
   replicates 9454a62b681adcf4580276c39159f24cc3c227d6
S: c5bf71c1dec51c3446eca58fa0a4f74f5b94f647 xx.xx.xx.xx:6481
   slots: (0 slots) slave
   replicates 8fde0b5a819e567f9911683b20ef04dc963d2697
S: 6452ec3bab699a6184af8e9f993ad97f24780d32 xx.xx.xx.xx:6480
   slots: (0 slots) slave
   replicates 604e6470d535310c29c7520a2f53109e416a9604
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

(2)为每个集群节点添加Slave,形成主从复制架构,主从复制架构可参考1.1.1.,搭建结构如下所示

6379(Master) 6479(Slave of 6379) 6579(Slave of 6379)
6380(Master) 6480(Slave of 6380) 6580(Slave of 6380)
6381(Master) 6481(Slave of 6381) 6581(Slave of 6381)

创建集群 使用如下命令, redis-trib.rb会默认为我们实现主从

./redis-trib.rb create --replicas 2 xx.xx.xx.xx:6379 xx.xx.xx.xx:6380 xx.xx.xx.xx:6381 xx.xx.xx.xx:6479 xx.xx.xx.xx:6480 xx.xx.xx.xx:6481 xx.xx.xx.xx:6579 xx.xx.xx.xx:6580 xx.xx.xx.xx:6581

–replicas参数指定集群中每个主节点配备几个从节点,这里设置为2
(3)集群模式自动实现高可用,自动完成主备切换
1.3.4.4自行测试高可用Cluster环境
注意在集群环境中:
多键的命令操作(如MGET、MSET),如果每个键都位于同一个节点,则可以正常支持,否则会提示错误。
集群中的节点只能使用0号数据库,如果执行SELECT切换数据库会提示错误。

1.3.5.集群模式原理

1.3.5.1.插槽的概念及插槽分配
这里先说一个概念
一致性hash算法:一般用于缓存服务器的负载均衡选取,相比于一般的hash算法,它会先通过缓存服务器的ip地址或者服务器名称获取hash值定位到hash数组的几个位置上。当我们存储缓存时通过key值得到hash值,以顺时针方向定位到最近的缓存服务。

整个Redis提供了16384个插槽,也就是说集群中的每个节点分得的插槽数总和为16384。./redis-trib.rb 脚本实现了是将16384个插槽平均分配给了N个节点。当我们执行set abc 123命令时,redis是如何将数据保存到集群中的呢?执行步骤:

i.接收命令set abc 123
ii.通过key(abc)计算出插槽值,然后根据插槽值找到对应的节点。abc的插槽值为:7638
iii.重定向到该节点执行命令

注意:如果插槽数有部分是没有指定到节点的,那么这部分插槽所对应的key将不能使用。

1.3.5.2.集群高可用
a、一个集群里面有M1、M2、M3三个节点,其中节点 M1包含 0 到 5500号哈希槽,节点M2包含5501 到 11000 号哈希槽,节点M3包含11001 到 16384号哈希槽。如果M2宕掉了,就会导致5501 到 11000 号哈希槽不可用,从而使整个集群不可用。
b、一个集群里面有M1-S1、M2-S2、M3-S3六个主从节点,其中节点 M1包含 0 到 5500号哈希槽,节点M2包含5501 到 11000 号哈希槽,节点M3包含11001 到 16384号哈希槽。如果是M2宕掉,集群便会选举S2为新节点继续服务,整个集群还会正常运行。当M2、S2都宕掉了,这时候集群就不可用了。
结论:redis集群至少需要一个备份节点,才能更好的保证集群的高可用。

2.springboot2.0调用redis高可用架构

2.1.依赖

(1)jedis

<!--spirngboot版本为2.x-->
<!-- 加载spring boot redis包,springboot2.0中直接使用jedis或者lettuce配置连接池,默认为lettuce连接池,这里使用jedis连接池 -->
<!-- 加载spring boot redis包 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<!-- 排除lettuce包,使用jedis代替-->
	<exclusions>
		<exclusion>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>

(2)Lettuce

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

说明:Jedis和Lettuce的比较:
Jedis :
直连模式,在多个线程间共享一个 Jedis 实例时是线程不安全的,如果想要在多线程环境下使用 Jedis,需要使用连接池,每个线程都去拿自己的 Jedis 实例,当连接数量增多时,物理连接成本较高。
Lettuce:
连接是基于Netty的,连接实例可以在多个线程间共享,所以,一个多线程的应用可以使用同一个连接实例,而不用担心并发线程的数量。当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。通过异步的方式可以让我们更好的利用系统资源,而不用浪费线程等待网络或磁盘I/O
(但是这里有个比较坑的地方,那就是commons-pool2默认节点和节点之间是通过内网ip来获取连接进行校验的,集群模式下在内网下做分布式部署会抛出连接超时的异常,目前未找到比较合适的修改方案所以目前节点和节点使用内网部署使用基于Jedis的连接池配置)

2.2.application.properties配置

(1)redis单机版(lettuce)

# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=xx.xx.xx.xx
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=root
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0

(2)redis哨兵模式

# Redis数据库索引(默认为0)
spring.redis.database=0
spring.redis.timeout=3000
# Redis服务器连接密码(默认为空)
spring.redis.password=root
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=2000
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=1000
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=500
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=50
#哨兵模式
#主服务器所在集群名称
spring.redis.sentinel.master=redis-test-master
spring.redis.sentinel.nodes=xx.xx.xx.xx:26379,xx.xx.xx.xx:26380,xx.xx.xx.xx:26381
#密码唯一
spring.redis.password=root

(3)集群模式
1)基于lettuce

#redis集群模式(该配置基于commons-pool2连接池,redis各个服务不在同一内网情况下无异常)
# Redis数据库索引(默认为0)
spring.redis.database=0
spring.redis.timeout=3000
# Redis服务器连接密码(默认为空)
spring.redis.password=root
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=2000
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=1000
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=500
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=50
#集群模式
spring.redis.cluster.nodes=xx.xx.xx.xx:6379,xx.xx.xx.xx:6380,xx.xx.xx.xx:6381,xx.xx.xx.xx:6479,xx.xx.xx.xx:6480,xx.xx.xx.xx:6481
#密码唯一
spring.redis.password=root

2)基于jedis

#集群模式(基于集群模式使用commons-pool2连接池会默认通过公网的内网进行访问,如果redis使用公网部署使用jedis使用以下配置)
spring.redis.cluster.nodes=xx.xx.xx.xx:6379,xx.xx.xx.xx:6380,xx.xx.xx.xx:6381,xx.xx.xx.xx:6479,xx.xx.xx.xx:6480,xx.xx.xx.xx:6481
spring.redis.password=root
#连接超时时间
spring.redis.timeout=6000ms
#Redis数据库索引(默认为0)
spring.redis.database=0
# 连接池配置,springboot2.0中直接使用jedis或者lettuce配置连接池,默认为lettuce连接池
#连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1s
#连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
#接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0

2.3.封装调用

(1)重写RedisTemplate

RedisAutoConfiguration源码

springboot redistemplate可以使用redis集群吗 springboot调用redis集群_Redis_34


通过源码可以看出,SpringBoot自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。但是,这个RedisTemplate的泛型是<Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为<String,Object>形式的RedisTemplate。并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。默认的泛型只支持<String, String>、<Object, Object>两种类型,这里我们可以对restTempldte进行重写

@Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
 
        // 使用Jackson2JsonRedisSerialize 替换默认的jdkSerializeable序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
 
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;

}

(2)常用封装

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
 
@Component
public class RedisCacheManager {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
 
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
 
    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
 
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
 
    }
 
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
 
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
 
    // ================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
 
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
 
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
 
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
 
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
 
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
 
    // ============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
 
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
 
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
 
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
 
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
 
    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 
    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
 
}
(3)简单测试
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.neo.config.RedisCacheManager;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisCacheManagerTest {
	@Autowired
    private RedisCacheManager redisCacheManager;
	
    @Test
    public void test() throws Exception {
    	redisCacheManager.set("hello", "hello", 1);
    	System.out.println(redisCacheManager.get("hello"));
    	Thread.sleep(1000);
    	System.out.println(redisCacheManager.get("hello"));
    }
}