在实际生产中,不可能Redis是以单机启动的,因为这样的服务是非常不稳定的,现在的项目首先提倡高可用,而高可用最佳的使用方式就是分布式部署(多部署几份以分摊意外的分享),而集群部署和主从复制是一个意思,Redis是通过主从复制来完成集群部署的,哨兵机制(Sentinel)又是在主从复制上的又一级别的改进了!
主从复制也就是当主服务(Master)更新时,从节点(Slave)也随之更新,这块也有一个数据库概念不得不提的,叫做读写分离,这一切的目的呢,都是为了可以更好地提高服务,保证高可用!
那么看一下Redis主从复制有什么优点:
再次说一下Redis不可以单机启动的原因:
注意:
每一台Redis服务默认自己就是Master,一个Master可以有多个Slave,但是Slave只可以有一个Master
这其中涉及到的命令如下
info replication # 显示当前redis节点状态
slaveof ip part # 设置从机,ip:主机ip;part:主机part
slaveof on one # 解除当前Redis服务的Slave角色
这两个命令就可以完成这个主从复制的部署了,因为我只有一台服务器,所以就不以服务器作为测试了,如果在一台机器上测试的话,其实就是使用不同的端口号去启用。不过我没有采用这种办法,我是在本地起了三个虚拟机,他们分别拥有三个虚拟的ip端口,实现下图的关系
主从分离,在Master写入,其他Slave读取
分别安装Redis并且配置好配置文件,如果这个不太清楚的,看我第一节写的linux安装Redis教程,这里开启Redis服务,并且查看一下Redis的状态,可以看出来现在是Master,其他两台机器和这个状态一致
自此就设置完了,配置这个是非常简单的,让我们测试一下以及研究一下目前存在的问题
这里试想一下,在这个简单的三个集群中,会不会出现以下问题呢?
关于这些问题,我直接答案写到这了,好奇的也可以自己去探索,这些结论也是我实践出来的,若有问题,望指教
Slave启动成功首次连接Master后会发送一个sync同步命令,Master接受到命令后,启动存盘进程去收集所有修改数据集的命令,再执行完毕后,Master会将这个文件发送给Slave,完成一次完全同步(全量复制)
全量复制: Slave收到Master的数据库文件,将其完全存盘到内存中
增量复制:Master持续将新的修改命令依次传给Slave,完成sync
关于哨兵模式这里,我整理了几篇文章来讲解一下这部分的内容,我认为这块简单的使用是非常方便的,在网络上摘抄一些配置内容就可以启动了,但是具体到其中的观念还是挺多的!以下内容不涉及哨兵相关的算法,这块超出了我目前探索的范围
哨兵机制(Sentinel)是基于Redis的主从复制的,也是为了解决主从复制所带来的问题,有句话说得好,程序里边就没有加一层解决不了的问题,就好比主从复制很方便,但是如果集群中某个机器宕机了,这就需要人工去操作重启和配置了,这里就是很不智能的地方!
那如何解决呢?当然是启用另外一个进程或者服务去监听Redis集群,如果发现宕机或者不可用,那么自动进行配置!而不是人工进行配置,这样就可以解决问题了
就好比如上图,当又有一个问题来了,那要是哨兵这个机器宕机了呢?这就是一个高可用的问题,凡是高可用的问题,大多数集群必定可以解决,搭建一个Sentinel集群就好了
所以程序的本质的处理方式都是相同的,或许语法不太相同,但是解决方案大同小异
优点:
缺点:
需要注意的是: SDOWN和ODOWN这种概念只适用于Master节点,Slave节点并不适用
这里涉及到了,Sentinel是如何来判断在Slave中选举出Master的,主要是根据以下几点
关于断开连接次数是很重要去否决一个Slave成为Master的条件
如果一个slave与master失去联系超过10次,并且每次都超过了配置的最大失联时间(down-after-milliseconds),如果sentinel在进行failover时发现slave失联,那么这个slave就会被sentinel认为不适合用来做新master的。
更严格的定义是,如果一个slave持续断开连接的时间超过
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
就会被认为失去选举资格。
符合上述条件的slave才会被列入master候选人列表,并根据以下顺序来进行排序:
一个redis无论是master还是slave,都必须在配置中指定一个slave优先级。要注意到master也是有可能通过failover变成slave的。
如果一个redis的slave优先级配置为0,那么它将永远不会被选为master。但是它依然会从master哪里复制数据。
例子:
当网络恢复以后,我们知道sentinel3将会更新它的配置。但是,如果客户端所连接的master被网络隔离,会发生什么呢?
min-slaves-to-write 1
min-slaves-max-lag 10
通过上面的配置,当一个redis是master时,如果它不能向至少一个slave写数据(上面的min-slaves-to-write指定了slave的数量),它将会拒绝接受客户端的写请求。
于复制是异步的,master无法向slave写数据意味着slave要么断开连接了,要么不在指定时间内向master发送同步数据的请求了(上面的min-slaves-max-lag指定了这个时间)。
################################# REPLICATION #################################
#复制选项,slave复制对应的master。
# slaveof
#如果master设置了requirepass,那么slave要连上master,需要有master的密码才行。masterauth就是用来配置master的密码,这样可以在连上master后进行认证。
# masterauth
#当从库同主机失去连接或者复制正在进行,从机库有两种运行方式:1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。2) 如果slave-serve-stale-data设置为no,除去INFO和SLAVOF命令之外的任何请求都会返回一个错误”SYNC with master in progress”。
slave-serve-stale-data yes
#作为从服务器,默认情况下是只读的(yes),可以修改成NO,用于写(不建议)。
slave-read-only yes
#是否使用socket方式复制数据。目前redis复制提供两种方式,disk和socket。如果新的slave连上来或者重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。有2种方式:disk方式是master创建一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。socket是master创建一个新的进程,直接把rdb文件以socket的方式发给slave。disk方式的时候,当一个rdb保存的过程中,多个slave都能共享这个rdb文件。socket的方式就的一个个slave顺序复制。在磁盘速度缓慢,网速快的情况下推荐用socket方式。
repl-diskless-sync no
#diskless复制的延迟时间,防止设置为0。一旦复制开始,节点不会再接收新slave的复制请求直到下一个rdb传输。所以最好等待一段时间,等更多的slave连上来。
repl-diskless-sync-delay 5
#slave根据指定的时间间隔向服务器发送ping请求。时间间隔可以通过 repl_ping_slave_period 来设置,默认10秒。
# repl-ping-slave-period 10
#复制连接超时时间。master和slave都有超时时间的设置。master检测到slave上次发送的时间超过repl-timeout,即认为slave离线,清除该slave信息。slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时。
# repl-timeout 60
#是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。默认是no,即使用tcp nodelay。如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。但是这也可能带来数据的延迟。默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。
repl-disable-tcp-nodelay no
#复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。这样在slave离线的时候,不需要完全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。没有slave的一段时间,内存会被释放出来,默认1m。
# repl-backlog-size 5mb
#master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。单位为秒。
# repl-backlog-ttl 3600
#当master不可用,Sentinel会根据slave的优先级选举一个master。最低的优先级的slave,当选master。而配置成0,永远不会被选举。
slave-priority 100
#redis提供了可以让master停止写入的方式,如果配置了min-slaves-to-write,健康的slave的个数小于N,mater就禁止写入。master最少得有多少个健康的slave存活才能执行写命令。这个配置虽然不能保证N个slave都一定能接收到master的写操作,但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。设置为0是关闭该功能。
# min-slaves-to-write 3
#延迟小于min-slaves-max-lag秒的slave才认为是健康的slave。
# min-slaves-max-lag 10
port 20086 #默认端口26379
dir "/tmp"
logfile "/var/log/redis/sentinel_20086.log"
daemonize yes
#格式:sentinel ;#该行的意思是:监控的master的名字叫做T1(自定义),地址为127.0.0.1:10086,行尾最后的一个2代表在sentinel集群中,多少个sentinel认为masters死了,才能真正认为该master不可用了。
sentinel monitor T1 127.0.0.1 10086 2
#sentinel会向master发送心跳PING来确认master是否存活,如果master在“一定时间范围”内不回应PONG 或者是回复了一个错误消息,那么这个sentinel会主观地(单方面地)认为这个master已经不可用了(subjectively down, 也简称为SDOWN)。而这个down-after-milliseconds就是用来指定这个“一定时间范围”的,单位是毫秒,默认30秒。
sentinel down-after-milliseconds T1 15000
#failover过期时间,当failover开始后,在此时间内仍然没有触发任何failover操作,当前sentinel将会认为此次failoer失败。默认180秒,即3分钟。
sentinel failover-timeout T1 120000
#在发生failover主备切换时,这个选项指定了最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave处于不能处理命令请求的状态。
sentinel parallel-syncs T1 1
#sentinel 连接设置了密码的主和从
#sentinel auth-pass xxxxx
#发生切换之后执行的一个自定义脚本:如发邮件、vip切换等
##sentinel notification-script ##不会执行,疑问?
#sentinel client-reconfig-script ##这个会执行
以上内容整理自Redis哨兵机制(sentinel),这个博主写的有点多,看了两三个小时,才理清楚,本来想总结整理一下的,但是发现如果想理解清晰透彻,有些内容确实不能少,所以只是整理了一部分,有些内容就照搬了
我是以虚拟机进行搭建集群的,分别在三台虚拟机上启动了一个Redis服务以及Sentinel服务,下面是详细的配置信息
# Slave配置一个Master的关联就可以了,其他的使用默认的就可以(毕竟是测试使用,没有那么多的要求)
replicaof 10.159.82.6 6379
sentinel monitor mymaster 10.159.82.6 6379 2
sentinel down-after-milliseconds mymaster 10000
然后依次启用所有Redis集群即可,记得这里要Master先启动,其他的顺序都没关系
redis启动命令
redis.server ./redis.conf
sentinel启动命令
redis.sentinel ./sentinel.conf
这种效果就可以了
之后就可以进行测试了,比如手动将Master宕机掉,看一下Sentinel是否会选举出一个Master,我这边是没问题的,这块只要配置文件没问题了,启动。。。。不可能出问题吧,这不可能吧
集群就是cluster,这块不得不说一下写这篇博客的辛酸,在写这篇博客之前,我一直以为由三个节点组成的集群就是cluster,结果大错特错,耽误了好长时间,不过这也让我查看了好多文章,对这部分内容理解更深刻了些
下面着重讲一下Redis中主从复制、sentinel、cluster的区别
搭建Cluster集群
首先需要准备六个节点,集群一般都是两个为一组,一共三组,这个在我看来也是最小的集群模型了!这六个节点可以在一个服务器中搭建,但是这个其实是一个伪集群,它并不能做到风险分摊。我这边是在三个虚拟机上搭建了六个节点,对了cluster集群搭建好了之后,它默认两个为一组的主从节点不是一个主机
这里他的参数很多,我测试所用只修改打开了他的cluster开关,其他内容采用了默认的形式
cluster-enabled yes #开启集群模式
cluster-config-file nodes-7000.conf #集群内部的配置文件
cluster-require-full-coverage no #redis cluster需要16384个slot都正常的时候才能对外提供服务,换句话说,只要任何一个slot异常那么整个cluster不对外提供服务。 因此生产环境一般为no
因为可能是在一个主机中进行的多个节点的分配,所以一定要修改port端口号和pid进程号,不要打开replication主从复制的配置
分别启动六个节点服务,这块我是直接写了一个脚本文件进行启动的
cd /opt/redis/6.0.9/conf/6379
./redis-server6379 ./redis6379.conf
cd /opt/redis/6.0.9/conf/6380
./redis-server6380 ./redis6380.conf
上边就是脚本文件redis-cluster.sh的内容,如果创建了脚本文件一定要给它授权后才能使用
chmod +x redis-cluster.sh
这样就可以直接启动脚本文件了
开启六个节点服务后,就可以直接搭建集群了,这块有一点需要注意的是,现在大多数的DSCN文章都是对以前的Redis版本进行的讲解,所以使用的是ruby搭建Redis的方法!但是在Redis5.0之后,使用了redis-cli就可以进行集群的搭建了
使用命令
redis-cli --cluster create 10.159.82.252:6379 10.159.82.252:6380 10.159.82.6:6379 10.159.82.6:6380 10.159.82.17:6379 10.159.82.17:6380 --cluster-replicas 1
cluster-replicas:从机数量
redis-cli --cluster create:后面跟着的是六个节点的ip和端口号
执行命令后如下
[root@localhost conf]# redis-cli --cluster create 10.159.82.252:6379 10.159.82.252:6380 10.159.82.6:6379 10.159.82.6:6380 10.159.82.17:6379 10.159.82.17:6380 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 10.159.82.6:6380 to 10.159.82.252:6379
Adding replica 10.159.82.17:6380 to 10.159.82.6:6379
Adding replica 10.159.82.252:6380 to 10.159.82.17:6379
M: 2f8d3e6906a2f925f1b9da164f3ca89b452760ad 10.159.82.252:6379
slots:[0-5460] (5461 slots) master
S: bf26ca8ce8db660493817b72a880c8de6c8d7bf3 10.159.82.252:6380
replicates 729a491dd4aaeb1b986e523fa21374ba42badc68
M: 6076e01590550da6cc56fb365f4757af3bcfaf51 10.159.82.6:6379
slots:[5461-10922] (5462 slots) master
S: 3b07433fe987dda2db812a1403f86b4a846cf874 10.159.82.6:6380
replicates 2f8d3e6906a2f925f1b9da164f3ca89b452760ad
M: 729a491dd4aaeb1b986e523fa21374ba42badc68 10.159.82.17:6379
slots:[10923-16383] (5461 slots) master
S: 56357f19fab6dd5e780f06f78decc067e60c9f75 10.159.82.17:6380
replicates 6076e01590550da6cc56fb365f4757af3bcfaf51
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 10.159.82.252:6379)
M: 2f8d3e6906a2f925f1b9da164f3ca89b452760ad 10.159.82.252:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: bf26ca8ce8db660493817b72a880c8de6c8d7bf3 10.159.82.252:6380
slots: (0 slots) slave
replicates 729a491dd4aaeb1b986e523fa21374ba42badc68
S: 3b07433fe987dda2db812a1403f86b4a846cf874 10.159.82.6:6380
slots: (0 slots) slave
replicates 2f8d3e6906a2f925f1b9da164f3ca89b452760ad
M: 6076e01590550da6cc56fb365f4757af3bcfaf51 10.159.82.6:6379
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 56357f19fab6dd5e780f06f78decc067e60c9f75 10.159.82.17:6380
slots: (0 slots) slave
replicates 6076e01590550da6cc56fb365f4757af3bcfaf51
M: 729a491dd4aaeb1b986e523fa21374ba42badc68 10.159.82.17:6379
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
创建集群的过程中可能会遇到各种问题,下面我简单举例几个常见的
[root@localhost conf]# redis-cli -p 6379 cluster nodes
729a491dd4aaeb1b986e523fa21374ba42badc68 10.159.82.17:6379@16379 myself,master - 0 1608292538000 5 connected 10923-16383
6076e01590550da6cc56fb365f4757af3bcfaf51 10.159.82.6:6379@16379 master - 0 1608292538376 3 connected 5461-10922
bf26ca8ce8db660493817b72a880c8de6c8d7bf3 10.159.82.252:6380@16380 slave 729a491dd4aaeb1b986e523fa21374ba42badc68 0 1608292539383 5 connected
3b07433fe987dda2db812a1403f86b4a846cf874 10.159.82.6:6380@16380 slave 2f8d3e6906a2f925f1b9da164f3ca89b452760ad 0 1608292540389 1 connected
2f8d3e6906a2f925f1b9da164f3ca89b452760ad 10.159.82.252:6379@16379 master - 0 1608292536000 1 connected 0-5460
56357f19fab6dd5e780f06f78decc067e60c9f75 10.159.82.17:6380@16380 slave 6076e01590550da6cc56fb365f4757af3bcfaf51 0 1608292537368 3 connected
[root@localhost conf]# redis-cli -p 6379 cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:5
cluster_stats_messages_ping_sent:538
cluster_stats_messages_pong_sent:573
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:1112
cluster_stats_messages_ping_received:573
cluster_stats_messages_pong_received:539
cluster_stats_messages_received:1112
127.0.0.1:6379> info cluster
# Cluster
cluster_enabled:1
127.0.0.1:6379> cluster help
1) CLUSTER <subcommand> arg arg ... arg. Subcommands are:
2) ADDSLOTS <slot> [slot ...] -- Assign slots to current node.
3) BUMPEPOCH -- Advance the cluster config epoch.
4) COUNT-failure-reports <node-id> -- Return number of failure reports for <node-id>.
5) COUNTKEYSINSLOT <slot> - Return the number of keys in <slot>.
6) DELSLOTS <slot> [slot ...] -- Delete slots information from current node.
7) FAILOVER [force|takeover] -- Promote current replica node to being a master.
8) FORGET <node-id> -- Remove a node from the cluster.
9) GETKEYSINSLOT <slot> <count> -- Return key names stored by current node in a slot.
10) FLUSHSLOTS -- Delete current node own slots information.
11) INFO - Return information about the cluster.
12) KEYSLOT <key> -- Return the hash slot for <key>.
13) MEET <ip> <port> [bus-port] -- Connect nodes into a working cluster.
14) MYID -- Return the node id.
15) NODES -- Return cluster configuration seen by node. Output format:
16) <id> <ip:port> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ... <slot>
17) REPLICATE <node-id> -- Configure current node as replica to <node-id>.
18) RESET [hard|soft] -- Reset current node (default: soft).
19) SET-config-epoch <epoch> - Set config epoch of current node.
20) SETSLOT <slot> (importing|migrating|stable|node <node-id>) -- Set slot state.
21) REPLICAS <node-id> -- Return <node-id> replicas.
22) SAVECONFIG - Force saving cluster configuration on disk.
23) SLOTS -- Return information about slots range mappings. Each range is made of:
24) start, end, master and replicas IP addresses, ports and ids
127.0.0.1:6379> cluster nodes
729a491dd4aaeb1b986e523fa21374ba42badc68 10.159.82.17:6379@16379 myself,master - 0 1608292718000 5 connected 10923-16383
6076e01590550da6cc56fb365f4757af3bcfaf51 10.159.82.6:6379@16379 master - 0 1608292717764 3 connected 5461-10922
bf26ca8ce8db660493817b72a880c8de6c8d7bf3 10.159.82.252:6380@16380 slave 729a491dd4aaeb1b986e523fa21374ba42badc68 0 1608292715751 5 connected
3b07433fe987dda2db812a1403f86b4a846cf874 10.159.82.6:6380@16380 slave 2f8d3e6906a2f925f1b9da164f3ca89b452760ad 0 1608292718771 1 connected
2f8d3e6906a2f925f1b9da164f3ca89b452760ad 10.159.82.252:6379@16379 master - 0 1608292717000 1 connected 0-5460
56357f19fab6dd5e780f06f78decc067e60c9f75 10.159.82.17:6380@16380 slave 6076e01590550da6cc56fb365f4757af3bcfaf51 0 1608292716757 3 connected
其他的根据help信息,应该可以了解
下面看一下在java程序中 如何连接redis集群
接下来的Springboot很简单,其实就是配置一些配置文件,写一些配置类,这块对于基础使用的朋友已经足够了,如果有深入想了解底层流程或者优化连接的朋友那估计要等半年左右我去解读一下Lettuce底层了,那时候没准你就先会了。
下面是我对Cluster的配置,如果是Sentinel 的话,那修改一下配置就好了。对了,我认为针对于上面Redis的三种集群模式,Spring Data 也有三种不同的连接方式,分别是读写分离、Sentinel连接,Cluster连接。有兴趣的朋友可以去了解一下,这边只有Cluster连接,重点说一下,这三种肯定是Cluster最完善,但是并不是Cluster最好,因为任何技术都要根据业务来说的,当你业务量比较少的时候,用Cluster那分明是大炮打鸟。。哈哈
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
spring:
redis:
# host: 127.0.0.1 # 单机配置
# port: 6379
timeout: 10000ms
database: 0
# sentinel: # Sentinel 配置
# master: mymaster
# nodes:
# - 10.159.82.252:26379 # Sentinel 地址
# - 10.159.82.17:26379
# - 10.159.82.6:26379
cluster: # 配置Cluster集群
nodes:
- 10.159.82.252:6379
- 10.159.82.17:6379
- 10.159.82.6:6379
- 10.159.82.252:6380
- 10.159.82.17:6380
- 10.159.82.6:6380
max-redirects: 2 # 获取失败 最大重定向次数
lettuce:
pool:
max-active: 1000 #连接池最大连接数(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
/**
* @program: redis-demo
* @description: Redis配置类
**/
@Configuration
@EnableCaching // 开启缓存支持
public class RedisConfig extends CachingConfigurerSupport {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
private static Logger logger = LoggerFactory.getLogger(RedisConfig.class);
@Override
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuffer sb = new StringBuffer();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
// 缓存管理器
@Override
@Bean
public CacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(lettuceConnectionFactory);
@SuppressWarnings("serial")
Set<String> cacheNames = new HashSet<String>() {
{
add("codeNameCache");
}
};
builder.initialCacheNames(cacheNames);
return builder.build();
}
/**
* 配置Redis 序列化
* @param redisConnectionFactory
* @return
*/
@Bean("redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
在实际工作中,不可能是使用RedisTemplate对Redis进行操作的,肯定是对RedisTemplate进行的又一层封装,这里有一个通用版的Redis Util类,需要的朋友自行拿走,不谢,点赞即可
/**
* @program: redis-demo
* @description: Redis 工具类,在实际使用的过程中,是对RedisTemplate进行再次封装,以简化开发,这里提供一个通用的Redis工具类
**/
@Component
public class RedisUtil {
private static Logger logger = LoggerFactory.getLogger(RedisCacheManager.class);
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
/**
* <b>功能:清库</b><br>
* <br>
*
* @return String
* @Author:pfyangf , 2018年5月25日
*/
public void flushDb() {
this.redisTemplate.execute(new RedisCallback<Object>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
connection.flushDb();
return "ok";
}
});
}
/**
* 指定缓存失效时间
*
* @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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
return false;
}
}
/**
* <b>功能:向通道发布数据</b><br>
* <br>
*
* @param channel
* @param message
* @return boolean
* @Author:pfyangf , 2018年3月13日
*/
public boolean publish(String channel, Serializable message) {
try {
this.redisTemplate.convertAndSend(channel, message);
return true;
} catch (Exception e) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
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) {
logger.error("执行程序异常:",e);
return 0;
}
}
}
这里测试的话,我是使用的测试类进行的,其实不管是单机版、Sentinel还是Cluster集群,对于java层面来说使用都是一致的,都是直接调用Util类即可,当然如果是读写分离的模式的话,那么我估计还需要在Util中做一些处理,但是展现给最上层来看的都是一致的。这样也是非常重要的编程思想——封装
/**
* 测试Redis集群
*/
@Test
public void test1(){
redisUtil.flushDb();
redisUtil.set("name","xiaoming");
System.out.println(redisUtil.get("name"));
}
在研究Redis的过程中,我看过很多教学视频以及各种相关文章,但是学习的更多难免产生了更多的疑惑,以下记录一下,望各位大神可以讨论答复一下
以后有时间研究SpringData-Redis源码的时候,我也会来追更的
本来想一天结束,没想到弄了一周,本来想这一节直接结束Redis了,结果看样子还需要一节来补充Redis的内容,下一节将讲解Redis的缓存击穿、缓存穿透、缓存雪崩、数据预热以及缓存模式