-
业务场景分析
-
redis四种搭建方式分析
-
哨兵模式服务搭建与测试
-
哨兵模式原理分析
-
spring boot整合哨兵模式初始化
-
redis常见问题分析
业务场景分析
-
高性能: 支持多条操作指令并发操作、批量信息查询,并高效返回查询结果
-
高并发:支持一定数量并发操作,1s并发1000个以上,redis并发数不高。暂时不需要考虑动态扩展问题
-
高可用:一台redis服务机器挂掉后,其他服务能够快速接管,并支持读写同步
redis四种集群方式分析
1、单机模式
这个最简单,一看就懂。
就是安装一个redis,启动起来,业务调用即可。具体安装步骤和启动步骤就不赘述了,网上随便搜一下就有了。
单机在很多场景也是有使用的,例如在一个并非必须保证高可用的情况下。
咳咳咳,其实我们的服务使用的就是redis单机模式,所以来了就让我改为哨兵模式。
说说单机的优缺点吧。
优点:
-
部署简单,0成本。
-
成本低,没有备用节点,不需要其他的开支。
-
高性能,单机不需要同步数据,数据天然一致性。
缺点:
-
可靠性保证不是很好,单节点有宕机的风险。
-
单机高性能受限于CPU的处理能力,redis是单线程的。
单机模式选择需要根据自己的业务场景去选择,如果需要很高的性能、可靠性,单机就不太合适了。
2、主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。
前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

主从模式配置很简单,只需要在从节点配置主节点的ip和端口号即可。
1 slaveof <masterip> <masterport>
2 # 例如
3 # slaveof 192.168.1.214 6379
启动主从节点的所有服务,查看日志即可以看到主从节点之间的服务连接。
从上面很容易就想到一个问题,既然主从复制,意味着master和slave的数据都是一样的,有数据冗余问题。
在程序设计上,为了高可用性和高性能,是允许有冗余存在的。这点希望大家在设计系统的时候要考虑进去,不用为公司节省这一点资源。
对于追求极致用户体验的产品,是绝对不允许有宕机存在的。
主从模式在很多系统设计时都会考虑,一个master挂在多个slave节点,当master服务宕机,会选举产生一个新的master节点,从而保证服务的高可用性。
主从模式的优点:
-
一旦 主节点宕机, 从节点 作为 主节点 的 备份 可以随时顶上来。
-
扩展 主节点 的 读能力,分担主节点读压力。
-
高可用基石:除了上述作用以外,主从复制还是哨兵模式和集群模式能够实施的基础,因此说主从复制是Redis高可用的基石。
也有相应的缺点,比如我刚提到的数据冗余问题:
-
一旦 主节点宕机, 从节点 晋升成 主节点,同时需要修改 应用方 的 主节点地址,还需要命令所有 从节点 去 复制 新的主节点,整个过程需要 人工干预。
-
主节点 的 写能力 受到 单机的限制。
-
主节点 的 存储能力 受到 单机的限制。
3、哨兵模式
刚刚提到了,主从模式,当主节点宕机之后,从节点是可以作为主节点顶上来,继续提供服务的。
但是有一个问题,主节点的IP已经变动了,此时应用服务还是拿着原主节点的地址去访问,这...
于是,在Redis 2.8版本开始引入,就有了哨兵这个概念。
在复制的基础上,哨兵实现了自动化的故障恢复。

如图,哨兵节点由两部分组成,哨兵节点和数据节点:
-
哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。
-
数据节点:主节点和从节点都是数据节点。
访问redis集群的数据都是通过哨兵集群的,哨兵监控整个redis集群。
一旦发现redis集群出现了问题,比如刚刚说的主节点挂了,从节点会顶上来。但是主节点地址变了,这时候应用服务无感知,也不用更改访问地址,因为哨兵才是和应用服务做交互的。
Sentinel 很好的解决了故障转移,在高可用方面又上升了一个台阶,当然Sentinel还有其他功能。
比如 主节点存活检测、主从运行情况检测、主从切换。
Redis的Sentinel最小配置是 一主一从。
说下哨兵模式监控的原理
每个Sentinel以 每秒钟 一次的频率,向它所有的 主服务器、从服务器 以及其他Sentinel实例 发送一个PING 命令。

如果一个 实例(instance)距离最后一次有效回复 PING命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel标记为 主观下线。
如果一个 主服务器 被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次 的频率确认 该主服务器是否的确进入了 主观下线 状态。
如果一个 主服务器 被标记为 主观下线,并且有 足够数量 的 Sentinel(至少要达到配置文件指定的数量)在指定的 时间范围 内同意这一判断,那么这个该主服务器被标记为 客观下线。
在一般情况下, 每个 Sentinel 会以每 10秒一次的频率,向它已知的所有 主服务器 和 从服务器 发送 INFO 命令。
当一个 主服务器 被 Sentinel标记为 客观下线 时,Sentinel 向 下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从10秒一次改为 每秒一次。
Sentinel和其他 Sentinel 协商 主节点 的状态,如果 主节点处于 SDOWN`状态,则投票自动选出新的主节点。将剩余的 从节点 指向 新的主节点 进行 数据复制。
当没有足够数量的 Sentinel 同意 主服务器 下线时, 主服务器 的 客观下线状态 就会被移除。当 主服务器 重新向 Sentinel的PING命令返回 有效回复 时,主服务器 的 主观下线状态 就会被移除。
哨兵模式的优缺点
优点:
-
哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
-
主从可以自动切换,系统更健壮,可用性更高。
-
Sentinel 会不断的检查 主服务器 和 从服务器 是否正常运行。当被监控的某个 Redis 服务器出现问题,Sentinel 通过API脚本向管理员或者其他的应用程序发送通知。
缺点:
-
Redis较难支持在线扩容,对于集群,容量达到上限时在线扩容会变得很复杂。
4、集群模式
主从不能解决故障自动恢复问题,哨兵已经可以解决故障自动恢复了,那到底为啥还要集群模式呢?
主从和哨兵都还有另外一些问题没有解决,单个节点的存储能力是有上限,访问能力是有上限的。
Redis Cluster 集群模式具有 高可用、可扩展性、分布式、容错 等特性。
Cluster 集群模式的原理
通过数据分片的方式来进行数据共享问题,同时提供数据复制和故障转移功能。
之前的两种模式数据都是在一个节点上的,单个节点存储是存在上限的。集群模式就是把数据进行分片存储,当一个分片数据达到上限的时候,就分成多个分片。
数据分片怎么分?
集群的键空间被分割为16384个slots(即hash槽),通过hash的方式将数据分到不同的分片上的。
1 HASH_SLOT = CRC16(key) & 16384
CRC16是一种循环校验算法,这里不是我们研究的重点,有兴趣可以看看。
这里用了位运算得到取模结果,位运算的速度高于取模运算。

有一个很重要的问题,为什么是分割为16384个槽?这个问题可能会被面试官随口一问
数据分片之后怎么查,怎么写?

读请求分配给slave节点,写请求分配给master,数据同步从master到slave节点。
读写分离提高并发能力,增加高性能。
如何做到水平扩展?
master节点可以做扩充,数据迁移redis内部自动完成。
当你新增一个master节点,需要做数据迁移,redis服务不需要下线。
举个栗子:上面的有三个master节点,意味着redis的槽被分为三个段,假设三段分别是0~7000,7001~12000、12001~16383。
现在因为业务需要新增了一个master节点,四个节点共同占有16384个槽。
槽需要重新分配,数据也需要重新迁移,但是服务不需要下线。
redis集群的重新分片由redis内部的管理软件redis-trib负责执行。redis提供了进行重新分片的所有命令,redis-trib通过向节点发送命令来进行重新分片。
如何做故障转移?

假如途中红色的节点故障了,此时master3下面的从节点会通过 选举 产生一个主节点。替换原来的故障节点。
此过程和哨兵模式的故障转移是一样的。
总结:
每种模式都有各自的优缺点,在实际使用场景中要根据业务特点去选择合适的模式。
redis是一个非常常用的中间件,作为一个使用者来说,学习成本一点不高。
如果作为一个很好的中间件去研究的话,还是有很多值得学习和借鉴的地方。比如redis的各种数据结构(动态字符串、跳跃表、集合、字典等)、高效的内存分配(jemalloc)、高效的IO模型等等。
每个点都可以深入研究,在后期设计高并发、高可用系统的时候融入进去。
根据服务搭建模式:选择哨兵方式搭建分布式服务
哨兵模式服务搭建与测试
搭建哨兵服务:一主(157)两从(49、158)
端口设置
|
主机157
|
从机49
|
从机158
|
服务端口
|
6379
|
6379
|
6379
|
哨兵端口
|
6380
|
6380
|
6380
|
主从配置说明(供参考)
# 可以通过命令看一下主从信息,v5.x版本前从是用slave表示,之后换成replication
# master和slave节点都可以查看
info replication
# 服务端口
port 6379
# 修改slave的redis.conf
replicaof 192.168.1.100 6379 #master的ip,master的端口
# 在slave配置上添加
masterauth icoding #主机的访问密码
# yes 主从复制中,从服务器可以响应客户端请求
# no 主从复制中,从服务器将阻塞所有请求,有客户端请求时返回“SYNC with master in progress”;
# 默认开启yes,在slave上配置
replica-serve-stale-data yes
# slave节点只允许read 默认就是 yes
# 这个配置只对slave节点才生效,对master节点没作用
# 默认开启并配置为yes,在slave上配置
replica-read-only yes
# slave根据指定的时间间隔向master发送ping请求
# 时间间隔可以通过 repl-ping-replica-period 来设置,默认10秒
# 默认没有开启,在slave的配置上打开
repl-ping-replica-period 10
# 复制连接超时时间。
# master和slave都有超时时间的设置。
# master检测到slave上次发送的时间超过repl-timeout,即认为slave离线,清除该slave信息。
# slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。
# 需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时。
# 默认没有开启,在slave的配置上打开
repl-timeout 60
# 是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。
# 默认是no,即使用tcp nodelay,允许小包的发送。对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择
# 如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。
# 但是这也可能带来数据的延迟。
# 默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。
# 默认开启并配置为no,在master上添加
repl-disable-tcp-nodelay no
# 复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。
# 这样在slave离线的时候,不需要完全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。
# 缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。
# 没有slave的一段时间,内存会被释放出来,默认1m。
# 默认没有开启,在master上配置
repl-backlog-size 5mb
# master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。
# 单位为秒。
# 默认没有开启,在master上配置
repl-backlog-ttl 3600
# 当master不可用,Sentinel会根据slave的优先级选举一个master。
# 最低的优先级的slave,当选master。
# 而配置成0,永远不会被选举。
# 注意:要实现Sentinel自动选举,至少需要2台slave。
# 默认开启,在slave上配置
replica-priority 100
# redis提供了可以让master停止写入的方式,如果配置了min-slaves-to-write,健康的slave的个数小于N,mater就禁止写入。
# master最少得有多少个健康的slave存活才能执行写命令。
# 这个配置虽然不能保证N个slave都一定能接收到master的写操作,但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。
# 设置为0是关闭该功能,默认也是0。
# 默认没有开启,在master上配置
min-replicas-to-write 2
# 延迟小于min-replicas-max-lag秒的slave才认为是健康的slave。
# 默认没有开启,在master上配置
min-replicas-max-lag 10
哨兵配置
(供参考)
#redis的安装目录下,配置sentinel.conf文件
#部署三个哨兵节点,同时监控一组redis服务(只有一个节点其实也可以,但风险较高)
拷贝到跟redis.conf同级目录下
#bind 127.0.0.1 192.168.1.1
#测试的时候放开访问保护
protected-mode no
port 6380 #默认是6380
daemonize yes
pidfile /var/run/redis-sentinel-6379.pid #pid 集群要分开
logfile /user/local/redis-6379/sentinel/redis-sentinel.log #日志,非常重要
dir /usr/local/redis-6379/sentinel #工作空间
sentinel monitor mymaster 127.0.0.1 6379 2 #需要监控主机的信息:mymaster-主机名 127.0.0.1 6379主机地址、端口 2-quorum,如果>=2个哨兵主观认为主机下降了,master才会被客观下线,才会选一个slave为master
sentinel auth-pass 主机名 主机密码 #访问主机名称和密码
sentinel down-after-milliseconds 主机名 5000 #挂了5s后重新选举
sentinel parallel-syncs 主机名 1 #所有slave同步新master的并行同步数量,如果1就一个一个同步,在同步过程中slave节点是阻塞的,等同步完了才会放开
sentinel failover-timeout 主机名 180000 #同一个master节点failover之间的时间间隔
搭建实例(可直接复用):
-
搭建主机master
157:
-
搭建从服务slave
49:
158:
-
搭建主机哨兵
157:
-
搭建从机哨兵
49:
158:
快速启动脚本:
哨兵模式服务测试
-
数据同步测试
157可写入数据、49和158可以同步复制数据,但两者不可写入数据
-
主机宕机、从机接管测试
157主节点关闭后,49自动升级变为主节点,158还是从节点但对应的master变成49
-
主机恢复测试
157主节点重启后,自动升级为从节点,对应的master为49
哨兵模式原理分析
简介
sentinel是redis高可用的解决方案,sentinel系统(N个sentinel实例,N >= 1)可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。
1. sentinel初始化
可以使用命令
redis-sentinel /path/to/sentinel.conf
或者
redis-server /path/to/sentinel.conf --sentinel
来启动sentinel
sentinel启动时,需要经过一下几个步骤
a. 初始化服务
sentinel本质上是一个特殊的redis服务,所以初始化的时候跟redis服务初始化差不多,不过有几点不一样;首先sentinel不会载入RDB或者AOF文件,因为sentinel根本不使用数据库,其次,sentinel不能使用数据库键值对方面的命令,例如set、del、flushdb等等,同时,sentinel也不能使用事务、脚本、RDB或者AOF持久化命令,最后,复制命令,发布与订阅命令,文件事件处理器,时间事件处理器等只能在sentinel内部使用。
b. 将普通redis代码转成sentinel专用代码
将redis服务的代码转成sentinel的专用代码,例如sentinel的command与redis的command命令表就不一样(redis很多命令,sentinel不需要)
c. 初始化sentinel状态
主要是初始化sentinelState结构,sentinelState里面保存了sentinel的所有功能和状态,sentinelState结构如下

d. 根据指定的配置文件,初始化sentinel监视的主服务器列表
其实就是初始化sentinelState中的masters属性,masters字典中记录了所有被监视的主服务器信息,其中键是服务器名字,值是服务对应的sentinelRedisInstance结构,主要有实例名字,运行id,实例地址,客观下线票数,主管下线的最大无响应时间等等


sentinelState中masters字典的大致结构如下:

e. sentinel创建与masters(所有master)之间的网络连接
创建与被监视的master的网络连接后,sentinel成为该master的客户端,它会向master发送命令,并从master的响应中获取master的信息。对于每个被监视的master,sentinel会向其创建两个异步的网络连接
命令连接,这个连接专门用于向master发送命令,并接收命令回复
订阅连接,专门订阅master服务的 sentinel:hello频道
2. 获取master信息
sentinel以每10秒一次的频率向master发送info命令,通过info的回复来分析master信息,master的回复主要包含了两部分信息,一部分是master自身的信息,一部分是master所有的slave(从)的信息,所以sentinel可以自动发现master的从服务。sentinel从master哪儿获取到的master自身信息以及master所有的从信息,将会更新到sentinel的sentinelState中及masters(sentinelRedisInstance结构)中的slaves字典中

3. 获取从服务器信息
当sentinel发现master有新的从服务时,不但为从服务创建相信的实例结构,而且还会创建连接到该从服务的命令连接和订阅连接,创建命令连接后,sentinel会10秒每次的向从服务发送info命令,并从回复信息中提取从服务ID、从服务角色、从服务所属的主服务的ip及端口、主从服务的连接状态、从服务的优先级、从服务的复制偏移量等信息;创建或者更新到从服务的sentinelRedisInstance结构。
4. 向被监视服务器发送询问命令
sentinel会以每两秒一次的频率向所有的被监视服务器(master和从服务)发送询问命令,命令格式如下
publish ___sentinel___:hello s_ip s_port s_runid s_epoch m_name m_ip m_port m_epoch
各个参数的解析如下
s_ip:sentinel的ip
s_port:sentinel的端口
s_runid:sentinel云心id
s_epoch:sentinel当前的配置纪元
m_name:主服务器名字
m_ip:主服务器ip
m_port:主服务器端口
m_epoch:主服务器纪元
5. 接收被监视服务器的频道信息
sentinel与被监视的服务之间,一方面,sentinel通过命令链接发送信息到频道,另一方面,通过订阅连接从频道中接收信息。
对于同一服务的多个sentinel,一个sentinel发送的信息,会被其他sentinel收到,用于更新对该sentinel以及被监视服务的认知,用于更新sentinelRedisInstance的sentinels字典信息(请看sentinelRedisInstance的数据结构)及master信息。


当sentinel通过频道发现新的sentinel时,不但会更新上图的sentinel字典,同时会与新的sentinel建立命令连接(不会建立订阅连接,没啥可订阅的,因为sentinel与master及从建立订阅连接,是用来发现新的sentinel,而sentinel之间是已知的,所以不需要订阅连接),最终,监视同一个服务的多个sentinel会互联形成一个网络。
6. 主观下线
首先解析一下什么叫主观下线,所谓主观下线,就是单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。
sentinel会以每秒一次的频率向所有与其建立了命令连接的实例(master,从服务,其他sentinel)发ping命令,通过判断ping回复是有效回复,还是无效回复来判断实例时候在线(对该sentinel来说是“主观在线”)。
sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度,如果实例在down-after-milliseconds毫秒内,返回的都是无效回复,那么sentinel回认为该实例已(主观)下线,修改其flags状态为SRI_S_DOWN。如果多个sentinel监视一个服务,有可能存在多个sentinel的down-after-milliseconds配置不同,这个在实际生产中要注意。
7. 客观下线
当sentinel监视的某个服务主观下线后,sentinel会询问其它监视该服务的sentinel,看它们是否也认为该服务主观下线,接收到足够数量(这个值可以配置)的sentinel判断为主观下线,既任务该服务客观下线,并对其做故障转移操作。
sentinel通过发送 SENTINEL is-master-down-by-addr ip port current_epoch runid,(ip:主观下线的服务id,port:主观下线的服务端口,current_epoch:sentinel的纪元,runid:*表示检测服务下线状态,如果是sentinel 运行id,表示用来选举领头sentinel)来询问其它sentinel是否同意服务下线。
一个sentinel接收另一个sentinel发来的is-master-down-by-addr后,提取参数,根据ip和端口,检测该服务时候在该sentinel主观下线,并且回复is-master-down-by-addr,回复包含三个参数:down_state(1表示已下线,0表示未下线),leader_runid(领头sentinal id),leader_epoch(领头sentinel纪元)。
sentinel接收到回复后,根据配置设置的下线最小数量,达到这个值,既认为该服务客观下线
8. 选举领头sentinel
一个redis服务被判断为客观下线时,多个监视该服务的sentinel协商,选举一个领头sentinel,对该redis服务进行古战转移操作。选举领头sentinel遵循以下规则:
所有的sentinel都有公平被选举成领头的资格
所有的sentinel都有且只有一次将某个sentinel选举成领头的机会(在一轮选举中),一旦选举某个sentinel为领头,不能更改
sentinel设置领头sentinel是先到先得,一旦当前sentinel设置了领头sentinel,以后要求设置sentinel为领头请求都会被拒绝
每个发现服务客观下线的sentinel,都会要求其他sentinel将自己设置成领头
当一个sentinel(源sentinel)向另一个sentinel(目sentinel)发送is-master-down-by-addr ip port current_epoch runid命令的时候,runid参数不是*,而是sentinel运行id,就表示源sentinel要求目标sentinel选举其为领头
源sentinel会检查目标sentinel对其要求设置成领头的回复,如果回复的leader_runid和leader_epoch为源sentinel,表示目标sentinel同意将源sentinel设置成领头
如果某个sentinel被半数以上的sentinel设置成领头,那么该sentinel既为领头
如果在限定时间内,没有选举出领头sentinel,暂定一段时间,再选举
9. 故障转移
故障转移分为三个主要步骤
a. 从下线的主服务的所有从服务里面挑选一个从服务,将其转成主服务
sentinel状态数据结构中保存了主服务的所有从服务信息,领头sentinel按照如下的规则从从服务列表中挑选出新的主服务
删除列表中处于下线状态的从服务
删除最近5秒没有回复过领头sentinel info信息的从服务
删除与已下线的主服务断开连接时间超过 down-after-milliseconds*10毫秒的从服务,这样就能保留从的数据比较新(没有过早的与主断开连接)
领头sentinel从剩下的从列表中选择优先级高的,如果优先级一样,选择偏移量最大的(偏移量大说明复制的数据比较新),如果偏移量一样,选择运行id最小的从服务
b. 已下线主服务的所有从服务改为复制新的主服务
挑选出新的主服务之后,领头sentinel 向原主服务的从服务发送 slaveof 新主服务 的命令,复制新master
c. 将已下线的主服务设置成新的主服务的从服务,当其回复正常时,复制新的主服务,变成新的主服务的从服务
同理,当已下线的服务重新上线时,sentinel会向其发送slaveof命令,让其成为新主的从
spring boot整合哨兵模式初始化
-
maven pom引用
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.13.0</version>
</dependency>
-
jedis方式
初始化池
package com.erayt.match.cache.redis;
import com.erayt.match.constant.Constant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import redis.clients.jedis.*;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 文件名: com.erayt.match.cache.redis-RedisPool </br>
* 文件简介: Redis链接池 </br>
*
* @author zdf </br>
* @date 2020/8/31 18:46 </br>
* Copyright (c) 2019 ERAYT. All rights reserved
*/
@Configuration
@Component
public class RedisPool {
/** LOGGER 日志 */
private static final Logger LOGGER = LoggerFactory.getLogger(RedisPool.class);
@Value("${redis.maxTotal:200}")
private int maxTotal;// 最大连接数
@Value("${redis.maxIdle:200}")
private int maxIdle;// 最大空闲连接数: 在jedispool中最大的idle状态(空闲的)的jedis实例的个数
@Value("${redis.minIdle:50}")
private int minIdle;// 最小空闲连接数: 在jedispool中最小的idle状态(空闲的)的jedis实例的个数
@Value("${redis.timeout:1000}")
private int timeout;// 链接超时时间
@Value("${redis.password:123}")
private String password;// redis服务端的ip
// private Boolean testOnBorrow = true;//在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true。则得到的jedis实例肯定是可以用的。
// private Boolean testReturn = false;//在return一个jedis实例的时候,是否要进行验证操作,如果赋值true。则放回jedispool的jedis实例肯定是可以用的。
private JedisPool pool;
/** redis集群地址 **/
@Value("${redis.addresses:127.0.0.1:2379}")
private String[] addresses;
/** 是否使用jediss **/
@Value("${redis.usePool:false}")
private boolean usePool = false;
@Value("${redis.mode:1}")
private int mode; // redis使用模式:1-单机 2-集群 3-哨兵
private int soTimeout = 10000; // 获取数据超时时间,单位毫秒
private int maxAttempts = 3; // 超时后最大重试次数]
/** jedis集群模式连接配置 **/
private JedisCluster jedisCluster;
/** jedis哨兵模式连接配置 **/
private JedisSentinelPool jedisSentinelPool ;
/** 主机名称 **/
@Value("${redis.masterName:match}")
private String masterName = "match";
/**
* 功能描述: 初始化Redis链接池 </br>
* <p>
*
* @return void
* @author zdf
* @date 2020/8/31
*/
@PostConstruct
private void initPool() {
LOGGER.info("[INF][STR]初始化Redis链接池开始,Addresses[{}], mode[{}]", addresses, mode);
if (!usePool) {
LOGGER.info("[jediss]不初始化使用jediss");
return;
}
switch (mode){
case Constant.MODE_SINGLE:
initJedisPool();
break;
case Constant.MODE_CLUSTER:
initJedisCluster();
break;
case Constant.MODE_SENTINEL:
initJedisSentinel();
break;
default:
LOGGER.error("[DEFAULT]redis传入模式[{}]不存在,请重新设置",mode);
break;
}
LOGGER.info("[INF][END]初始化Redis链接池结束-Addresses[{}], mode[{}]", addresses, mode);
}
/**
* 功能描述: Redis集群模式初始化 </br>
* <p>
*
* 逗号分隔的ip地址:端口(ip:port)
* @return void
* @author zyj
* @date 2020/9/27
*/
private void initJedisSentinel() {
//初始化配置
JedisPoolConfig poolConfig = getJedisPoolConfig();
//地址转换
Set<String> adds=new HashSet<>(10);
adds.addAll(Arrays.asList(addresses));
//新建哨兵连接pool
jedisSentinelPool = new JedisSentinelPool(masterName, adds, poolConfig,timeout, password);
}
/**
* 功能描述: Redis单机模式的链接池初始化 </br>
* <p>
*
* ip地址:端口(ip:port)
* @return void
* @author zdf
* @date 2020/9/27
*/
private void initJedisPool() {
JedisPoolConfig poolConfig = getJedisPoolConfig();
String[] addressInfo = addresses[0].split(String.valueOf(Constant.QUOTATION));
pool = new JedisPool(poolConfig, addressInfo[0], Integer.parseInt(addressInfo[1]), timeout, password);
}
/**
* 功能描述: Redis集群模式初始化 </br>
* <p>
*
* 逗号分隔的ip地址:端口(ip:port)
* @return void
* @author zdf
* @date 2020/9/27
*/
private void initJedisCluster() {
Set<HostAndPort> nodeSet = new HashSet<>(addresses.length);
// 分割出集群节点
for (String node : addresses) {
String[] hp = node.split(String.valueOf(Constant.QUOTATION));
nodeSet.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
}
JedisPoolConfig poolConfig = getJedisPoolConfig();
jedisCluster = new JedisCluster(nodeSet, timeout, soTimeout, maxAttempts, password, poolConfig);
}
/**
* 功能描述: Redis公共配置参数 </br>
* <p>
*
* @return redis.clients.jedis.JedisPoolConfig
* @author zdf
* @date 2020/9/27
*/
private JedisPoolConfig getJedisPoolConfig() {
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接数
jedisPoolConfig.setMaxTotal(maxTotal);
// 最大空闲连接数
jedisPoolConfig.setMaxIdle(maxIdle);
// 最小空闲连接数
jedisPoolConfig.setMinIdle(minIdle);
return jedisPoolConfig;
}
/**
* 功能描述: 从Redis链接池获取一个链接 </br>
* 使用场景: 获取Jedis实例 </br>
* <p>
*
* @return redis.clients.jedis.Jedis
* @author zyj
* @date 2020/9/28
*/
public Jedis getJedis() {
if(Constant.MODE_SENTINEL==mode){
return jedisSentinelPool.getResource();
}
return pool.getResource();
}
/**
* 功能描述: 释放Jedis连接资源 </br>
* <p>
*
* @param jedis
* @return void
* @author zdf
* @date 2020/9/15
*/
public void closeJedis(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
/**
* 功能描述: 获取Redis集群下的Cluster </br>
* 使用场景: 获取JedisCluster实例 </br>
* <p>
*
* @return redis.clients.jedis.JedisCluster
* @author zdf
* @date 2020/9/27
*/
public JedisCluster getJedisCluster() {
return jedisCluster;
}
public boolean isUsePool() {
return usePool;
}
/**
* 功能描述: 获取Redis配置模式 </br>
* <p>
*
* @return int
* @author zdf
* @date 2020/9/27
*/
public int getMode() {
return mode;
}
}
初始化lua脚本
package com.erayt.match.cache.redis;
import com.erayt.match.constant.Constant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import redis.clients.jedis.Jedis;
import javax.annotation.PostConstruct;
import java.util.Map;
/**
* 文件名: com.erayt.match.cache.redis-LuaConfiguration </br>
* 文件简介: 初始化加载lua脚本 </br>
*
* @author zdf </br>
* @date 2020/8/29 11:51 </br>
* Copyright (c) 2019 ERAYT. All rights reserved
*/
@Configuration
@DependsOn("redisPool")
public class LuaConfiguration {
/** logger 日志 */
private static final Logger LOGGER = LoggerFactory.getLogger(LuaConfiguration.class);
@Autowired
private RedisPool pool;
@PostConstruct
public void initLuaScript() {
LOGGER.info("[INF][STR]初始化lua脚本开始,编译生成sha1,是否使用jedis[{}]",pool.isUsePool());
if(!pool.isUsePool()){
LOGGER.info("[END]无需初始化加载lua脚本");
return;
}
Jedis jedis = null;
try {
Map<String, String> luaScriptMap = LuaScriptMap.INSTANCE.getLuaScriptMap();
String queryBuySellLua = "local result = {}\n"
+ "result[1] = redis.call('ZRANGE', KEYS[1] , 0, -1, 'WITHSCORES')\n"
+ "result[2] = redis.call('ZRANGE', KEYS[2] , 0, -1, 'WITHSCORES')\n" + "return result;";
String orderLua = "local key = KEYS[1]\n" + "local oldMember = ARGV[1]\n" + "local score = tonumber(ARGV[2])\n"
+ "local newMember = ARGV[3]\n" + "redis.call('ZREM', key, oldMember)\n"
+ "return redis.call('ZADD', key, score, newMember)\n";
String orderCancelLua = "local key = KEYS[1]\n" + "local oldMemberPattern = ARGV[1]\n"
+ "local oldData = redis.call('ZSCAN', key, 0, 'MATCH', oldMemberPattern)\n"
+ "local oldMember = oldData[2][1]\n" + "if oldMember == nil then\n"
+ " redis.log(redis.LOG_NOTICE, \"not match oldData,key=\" .. key .. \", oldMemberPattern=\" .. oldMemberPattern)\n"
+ " return nil\n" + "end\n" + " redis.call('ZREM', key, oldMember)\n" + "return oldMember \n";
jedis = pool.getJedis();
luaScriptMap.put(Constant.BUY_SELL_LUA_SHA, jedis.scriptLoad(queryBuySellLua));
luaScriptMap.put(Constant.ORDER_DEAL_PART_LUA_SHA, jedis.scriptLoad(orderLua));
luaScriptMap.put(Constant.ORDER_CANCEL_LUA_SHA, jedis.scriptLoad(orderCancelLua));
} finally {
pool.closeJedis(jedis);
}
LOGGER.info("[INF][END]初始化lua脚本结束");
}
}
操作方法初始化
package com.erayt.match.cache.redis.handler;
import com.erayt.match.annotation.RedisClusterType;
import com.erayt.match.cache.redis.LuaScriptMap;
import com.erayt.match.cache.redis.RedisPool;
import com.erayt.match.constant.Constant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.Tuple;
import java.util.*;
/**
* 文件名: com.erayt.match.cache.redis.handler-JedisSentinelOperateHandler </br>
* 文件简介: Redis操作接口实现-Redis哨兵部署 </br>
*
* @author zyj </br>
* @date 2020/9/25 17:34 </br>
* Copyright (c) 2019 ERAYT. All rights reserved
*/
@Component
@RedisClusterType(Constant.MODE_SENTINEL)
public class JedisSentinelOperateHandler implements RedisOperateHandler {
/** logger 日志 */
private static final Logger LOGGER = LoggerFactory.getLogger(JedisSentinelOperateHandler.class);
@Autowired
private RedisPool redisPool;
/**
* 功能描述: 获取Jedis实例 </br>
* <p>
*
* @return redis.clients.jedis.Jedis
* @author zdf
* @date 2020/9/15
*/
private Jedis getJedis() {
return redisPool.getJedis();
}
/**
* 功能描述: 释放Jedis连接资源 </br>
* <p>
*
* @param jedis
* @return void
* @author zdf
* @date 2020/9/15
*/
private void closeJedis(Jedis jedis) {
redisPool.closeJedis(jedis);
}
/**
* 功能描述: 执行订单新增的lua脚本:订单先删再新增 </br>
* 使用场景: 部分成交 </br>
* <p>
*
* @param key
* @param oldMember
* 原订单Member
* @param newMember
* 新订单Member
* @param score
* 新订单score
* @return long 0-update 1-insert
* @author zdf
* @date 2020/8/31
*/
@Override
public long exeOrderLua(String key, String oldMember, String newMember, double score) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]exe lua start, key[{}], oldMember[{}], newMember[{}], score[{}]", key, oldMember,
newMember, score);
jedis = getJedis();
Object count = jedis.evalsha(getLuaSha1(Constant.ORDER_DEAL_PART_LUA_SHA), Collections.singletonList(key),
Arrays.asList(oldMember, String.valueOf(score), newMember));
LOGGER.info("[INF][END]exe lua end, count[{}], key[{}], oldMember[{}], newMember[{}], score[{}]", count,
key, oldMember, newMember, score);
return count == null ? -1 : (long) count;
} finally {
this.closeJedis(jedis);
}
}
private String getLuaSha1(String luaScriptKey) {
return LuaScriptMap.INSTANCE.getLuaScriptMap().get(luaScriptKey);
}
/**
* 功能描述: 执行撤销订单的lua脚本:先根据原订单流水号查找原订单的Member(正则表达式),再根据Member删除原订单 </br>
* 使用场景: 撤销订单 </br>
* <p>
*
* @param key
* 原数据Key
* @param oldOrderId
* 原订单号
* @return java.lang.String 订单member:订单流水号#剩余数量#客户号#总数量
* @author zdf
* @date 2020/8/31
*/
@Override
public String exeOrderCancelLua(String key, String oldOrderId) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]exe orderCancelLua start, key[{}], oldOrderId[{}]", key, oldOrderId);
String memberPattern = spliceMemberPattern(oldOrderId);
jedis = getJedis();
Object orderMember = jedis.evalsha(getLuaSha1(Constant.ORDER_CANCEL_LUA_SHA),
Collections.singletonList(key), Collections.singletonList(memberPattern));
LOGGER.info("[INF][END]exe orderCancelLua end, orderMember[{}], key[{}], memberPattern[{}]", orderMember,
key, memberPattern);
return (String) orderMember;
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 根据原订单号拼接正则表达式的member </br>
* <p>
*
* @param orderId
* 原订单号
* @return java.lang.String
* @author zdf
* @date 2020/9/15
*/
private String spliceMemberPattern(String orderId) {
return orderId + Constant.ASTERISK;
}
/**
* 功能描述: 执行查询有序集中买卖方向的lua脚本 </br>
* 使用场景: 获取有序集合数据买卖两个key的数据 </br>
* 处理流程: </br>
* 1、返回的是嵌套的List,外层List是序号,内层List(member1,score1.....memberN,scoreN) ;</br>
* <p>
*
* @param buyKey
* @param sellKey
* @return java.util.List<java.lang.String>
* @author zdf
* @date 2020/8/31
*/
@Override
public List<String> exeZRangeBuySellLua(String buyKey, String sellKey) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]exe ZRangeBuySell start, buyKey[{}], sellKey[{}]", buyKey, sellKey);
jedis = getJedis();
List<String> strList = (List<String>) jedis.evalsha(getLuaSha1(Constant.BUY_SELL_LUA_SHA),
Arrays.asList(buyKey, sellKey), new ArrayList<>());
LOGGER.info("[INF][END]exe ZRangeBuySell end, buyKey[{}], sellKey[{}]", buyKey, sellKey);
return strList;
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 向Redis的有序集合中增加数据:如果数据存在则会用新的score替换原来的,返回0;如果数据不存在则新增一个,返回1 </br>
* 使用场景: Redis有序集合增加数据 </br>
* <p>
*
* @param key
* @param score
* @param member
* @return boolean 1-新增成功 0-更新成功
* @author zdf
* @date 2020/8/29
*/
@Override
public long addZSet(String key, double score, String member) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]Redis add or update ZSet start..key[{}], score[{}], member[{}]", key, score, member);
jedis = getJedis();
Long zaddCount = jedis.zadd(key, score, member);
LOGGER.info("[INF][END]Redis add or update ZSet success..zaddCount[{}]", zaddCount);
return zaddCount;
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 删除数据:根据Key和Member删除有序集合中的数据 </br>
* 使用场景: Redis有序集合删除数据 </br>
* <p>
*
* @param key
* @param member
* @return void
* @author zdf
* @date 2020/8/31
*/
@Override
public void removeZSet(String key, String member) {
Jedis jedis = null;
try {
jedis = getJedis();
Long delCount = jedis.zrem(key, member);
LOGGER.info("[INF]Redis remove ZSet success..delCount[{}], Key[{}], Member[{}]", delCount, key, member);
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 删除数据:key组合删除多条redis数据 </br>
* 使用场景: redis集合删除多条数据 </br>
* <p>
*
* @param keys
* @return void
* @author zhouyunjian
* @date 2020/9/15
*/
@Override
public void removeZSetByKeys(String... keys) {
Jedis jedis = null;
try {
jedis = getJedis();
Long delCount = jedis.del(keys);
LOGGER.info("[INF]Redis remove ZSet By keys success..delCount[{}], Key[{}]", delCount, keys);
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 根据Key获取有序集合中所有数据:按Score由小到大顺序返回 </br>
* 使用场景: 获取数据(正序) </br>
* <p>
*
* @param key
* @return java.util.Set<java.lang.String>
* @author zdf
* @date 2020/8/31
*/
@Override
public Set<String> range(String key) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]query zset range data start, key[{}]", key);
jedis = getJedis();
Set<String> strs = jedis.zrange(key, 0, -1);
LOGGER.info("[INF][END]query zset range data end, key[{}]", key);
return strs;
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 根据Key获取有序集合中所有数据:按Score由大到小顺序返回 </br>
* 使用场景: 获取数据(倒序) </br>
* <p>
*
* @param key
* @return java.util.Set<java.lang.String>
* @author zdf
* @date 2020/8/31
*/
@Override
public Set<String> reverseRange(String key) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]query zset reverseRange data start, key[{}]", key);
jedis = getJedis();
Set<String> strs = jedis.zrevrange(key, 0, -1);
LOGGER.info("[INF][END]query zset reverseRange data end, key[{}]", key);
return strs;
} finally {
this.closeJedis(jedis);
}
}
/**
* 功能描述: 根据Key和订单流水号模糊查询Redis数据,返回Member值(订单流水号#剩余数量#客户号#总数量) </br>
* 使用场景: 流水号模糊查询 </br>
* <p>
*
* @param key
* @param oldOrderId
* @return java.lang.String 订单member:订单流水号#剩余数量#客户号#总数量
* @author zdf
* @date 2020/9/15
*/
@Override
public String zscan(String key, String oldOrderId) {
Jedis jedis = null;
try {
LOGGER.info("[INF][STR]query zset reverseRange data start, key[{}], oldOrderId[{}]", key, oldOrderId);
String memberPattern = spliceMemberPattern(oldOrderId);
jedis = getJedis();
String cursor = ScanParams.SCAN_POINTER_START;
ScanParams scanParams = new ScanParams();
scanParams.match(memberPattern);
ScanResult<Tuple> scanResult = jedis.zscan(key, cursor, scanParams);
if (CollectionUtils.isEmpty(scanResult.getResult())) {
LOGGER.info("[INF][END][WAR]query zset reverseRange data end, key[{}], memberPattern[{}], result=null",
key, memberPattern);
return null;
}
String orderMember = scanResult.getResult().get(0).getElement();
LOGGER.info("[INF][END]query zset reverseRange data end, key[{}], memberPattern[{}], orderMember[{}]", key,
memberPattern, orderMember);
return orderMember;
} finally {
this.closeJedis(jedis);
}
}
}
-
redisson方式
初始化池
package com.erayt.match.cache.redis;
import com.erayt.match.constant.Constant;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* 文件名: com.erayt.match.cache.redis-RedisConfig </br>
* 文件简介: redisson分布式锁初始化配置,应用于redis分布式锁处理 </br>
* 支持单机模式、哨兵模式、集群模式
*
* @author len </br>
* @date 2020/9/7 17:10 </br>
* Copyright (c) 2019 ERAYT. All rights reserved
*/
@Configuration
@Component
public class RedissonConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(RedissonConfig.class);
/** 数据区 **/
@Value("${redisson.database:1}")
private int database;
/** 登录密码 **/
@Value("${redisson.password:123456}")
private String password;
/** 超时时间 **/
@Value("${redisson.timeout:1000}")
private int timeout;
/** 看门狗超时 **/
private int watchDogTimeout=30000;
/** 最小活跃数 **/
private int minIdle = 1;
/** 最大活跃数 **/
private int maxIdle = 1;
/** 线程数 **/
private int threads = 1;
/** netty线程数 **/
private int nettyThreads = 1;
/** 主从、集群地址 **/
@Value("${redisson.addresses:127.0.0.1:2379}")
private String[] addresses;
/** 主机名称 **/
@Value("${redisson.masterName:match}")
private String masterName = "match";
/** redis模式:1-单机 2-集群 3-哨兵 **/
@Value("${redisson.mode:1}")
private int mode=1;
/** 是否使用redisson**/
@Value("${redisson.use:false}")
private volatile boolean use=false;
@Bean
RedissonClient redissonCient() {
RedissonClient redissonClient = null;
if(!use){
LOGGER.info("[redisson]不使用redisson");
return redissonClient;
}
switch (mode) {
case Constant.MODE_SINGLE:
redissonClient = redisSingle();
break;
case Constant.MODE_SENTINEL:
redissonClient = redisSendinel();
break;
case Constant.MODE_CLUSTER:
redissonClient = redisCluster();
break;
default:
redissonClient = redisSingle();
break;
}
return redissonClient;
}
// 单机模式
RedissonClient redisSingle() {
LOGGER.info("[redissonConfig]单机模式加载,地址[{}]password[{}]", addresses, password);
// 初始化配置:地址、用户名密码
Config config =initConfig();
// 设置单机模式
config.useSingleServer()
// 设置数据位置
.setDatabase(database)
// 服务地址
.setAddress(buildMultiAddresses(addresses)[0])
// 密码
.setPassword(password)
// 最小活跃数
.setConnectionMinimumIdleSize(minIdle)
// 最大活跃数
.setConnectionPoolSize(maxIdle)
// 超时时间
.setTimeout(timeout);
return Redisson.create(config);
}
// 哨兵模式
RedissonClient redisSendinel() {
LOGGER.info("[redissonConfig]哨兵模式加载,地址[{}]password[{}]", addresses, password);
// 初始化配置:地址、用户名密码
Config config = initConfig();
// 设置单机模式
config.useSentinelServers()
// 设置数据位置
.setDatabase(database)
// 服务地址
.addSentinelAddress(buildMultiAddresses(addresses))
// 密码
.setPassword(password)
// master名称
.setMasterName(masterName)
// master最小活跃数
.setMasterConnectionMinimumIdleSize(minIdle)
// master最大活跃数
.setMasterConnectionPoolSize(maxIdle)
// slave最小活跃数
.setSlaveConnectionMinimumIdleSize(minIdle)
// slave最大活跃数
.setSlaveConnectionPoolSize(maxIdle)
// 返回超时
.setTimeout(timeout)
// 连接超时
.setConnectTimeout(timeout);
return Redisson.create(config);
}
// 集群模式
RedissonClient redisCluster() {
LOGGER.info("[redissonConfig]集群模式加载,地址[{}]password[{}]", addresses, password);
// 初始化配置:地址、用户名密码
Config config = initConfig();
// 设置单机模式
config.useClusterServers()
// 服务地址
.addNodeAddress(buildMultiAddresses(addresses))
// 密码
.setPassword(password)
// master最小活跃数
.setMasterConnectionMinimumIdleSize(minIdle)
// master最大活跃数
.setMasterConnectionPoolSize(maxIdle)
// slave最小活跃数
.setSlaveConnectionMinimumIdleSize(minIdle)
// slave最大活跃数
.setSlaveConnectionPoolSize(maxIdle)
// 返回超时
.setTimeout(timeout)
// 连接超时
.setConnectTimeout(timeout);
return Redisson.create(config);
}
/** config参数设置 **/
private Config initConfig() {
Config config=new Config();
//线程池数量:线程池数量被所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务共同共享
config.setThreads(threads)
//这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,以及底层客户端所一同共享的线程池里保存的线程数量
.setNettyThreads(nettyThreads)
//看门狗超时时间:30 000ms
.setLockWatchdogTimeout(watchDogTimeout);
return config;
}
/** 哨兵/集群服务地址 **/
private String[] buildMultiAddresses(String[] addresses) {
// 新服务地址
String[] newAdds = new String[addresses.length];
for (int i = 0; i < addresses.length; i++) {
StringBuilder str = new StringBuilder();
str.append(Constant.REDIS_ADDRESS_MARK).append(addresses[i]);
newAdds[i] = str.toString();
}
return newAdds;
}
}
机器锁加锁方式(watch dog)实现
package com.erayt.match.cache.redis;
import com.erayt.match.cache.local.ManageAllCache;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 文件名: com.erayt.match.cache.redis-RedisLockManage </br>
* 文件简介: Redis锁管理服务
* redis加锁服务:redis加锁、维持锁、接管锁、释放锁服务
* redis加锁判定:对外提供redis加锁状态位
* @author len </br>
* @date 2020/8/27 17:09 </br>
* Copyright (c) 2019 ERAYT. All rights reserved
*/
@Component
public class RedisLockManage implements Runnable{
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockManage.class);
/**redis机器锁名称:多台机器抢夺同一个锁***/
@Value("${redisson.lockKey:redisLock}")
private String lockKey;
/**redis锁延时:锁到期后默认延时5s***/
@Value("${redisson.delayTime:5}")
private int delayTime;
/**redis锁接管:锁挂掉后被其他服务快速接管:默认在5s接管***/
@Value("${redisson.deliveTime:5}")
private int deliveTime;
/**redis锁加载:redis服务锁是否加载***/
@Value("${redisson.useLock:false}")
private volatile boolean use;
/**redisson客户端***/
@Autowired(required = false)
private RedissonClient redissonCient;
/**机器是否已经加锁:AtomicBoolean保证原子性操作***/
private AtomicBoolean lockFlag=new AtomicBoolean(false);
/**本地服务***/
private Thread thread;
/**第一次抢到锁释放缓存信息,防止服务重启持有锁后保留未处理旧数据:0-锁断开过 1-第一次拿到锁定-需要清空缓存 >1多次拿到锁,无需清空***/
private AtomicInteger first=new AtomicInteger();
@PostConstruct
public void init(){
LOGGER.info("[redislock][init]服务锁初始化加载开始");
if(!use){
LOGGER.info("[redislock][init]服务锁无需初始化");
return;
}
thread = new Thread(this, RedisLockManage.class + "-" + new SecureRandom().nextInt(100));
thread.setDaemon(true);
thread.start();
LOGGER.info("[redislock][init]服务锁初始化加载完成");
}
@Override
public void run() {
while (use){
try {
manageLock();
}catch (Exception e){
LOGGER.error("[error][redislock]redis finally 解锁异常,等待[{}]s后再处理",delayTime);
try {
TimeUnit.SECONDS.sleep(delayTime);
} catch (InterruptedException interruptedException) {
LOGGER.error("[error][redislock]redis等待被打断异常[{}]",e.getMessage(),e);
}
}
}
}
/**
* 功能描述: redis锁管理。系统启动时多台服务去抢一把锁,并不断延迟此锁的过期时间;</br>
* 其他未抢到锁的服务在不断检测此锁是否存在;如果不存在则抢锁,如果存在在继续等待。</br>
* 使用场景: 机器持有锁 </br>
* 处理流程: </br>
* 1、服务启动加锁,并设置锁超时时间(watch dog机制会自动管理超市处理);</br>
* 2、服务轮询,如果没有抢到锁,则每隔5s等待后重新获取一次,直到接管此锁 ;</br>
*<p>
* 测试场景:
* 1、多机抢锁测试
* 2、单机器挂掉,其他机器接管测试
* 3、网络闪断情况下,到恢复过程中,重新抢锁测试
* 4、机器正常关闭,是否快速释放锁,他机快速接管锁
*
* @author len
* @date 2020/9/7
*/
public void manageLock() {
// 获取锁状态是否以及锁定
RLock lock = redissonCient.getLock(lockKey);
LOGGER.info("[redislock][start]redis抢锁开始,判断本机是否持有锁[{}]当前key[{}]是否被锁定[{}]", lockFlag.get(), lockKey, lock.isLocked());
first.set(0);
try {
// 有锁等待5s,watch dog 默认延时30s,如果到时间程序未结束,则自动续命
lockFlag.set(lock.tryLock(delayTime, TimeUnit.SECONDS));
// lock.tryLock(30, delayTime, TimeUnit.SECONDS); 此方式未最多等待30s,到时间后不会自动续命
// 如果获取到锁,则服务进入等待,保证金不让服务停止
while (lockFlag.get()) {
LOGGER.debug("[redislock]本机获取到服务锁[{}],进入等待,一直维持持有锁", lockFlag.get());
//第一次拿到锁,清空一次缓存
if(first.addAndGet(1)==1){
ManageAllCache.clearAllCache();
}
//循环等待5s
TimeUnit.SECONDS.sleep(delayTime);
LOGGER.info("[redislock]本机等待结束,继续往下走,此时持有锁状态[{}]", lockFlag.get());
//如果use为false,跳出循环,此处当服务关闭时处理
//如果网络闪断,超过30s,会导致加锁断开,需要重新抢一次
lock = redissonCient.getLock(lockKey);
if(!lock.isLocked()){
LOGGER.info("[redislock]本机加锁[{}]连接断开", lockFlag.get());
manageLock();
first.set(0);
}
}
while (!lockFlag.get()) {
LOGGER.info("[redislock]本机未获取到服务锁[{}],[{}]s后重新尝试获取一次", lockFlag.get(),deliveTime);
// 等待1s后重新加锁
TimeUnit.SECONDS.sleep(deliveTime);
manageLock();
first.set(0);
}
} catch (InterruptedException ie) {
LOGGER.error("[error][redislock]redis锁等待打断异常[{}][{}]", ie.getMessage(), ie);
} catch (RedisException ie) {
LOGGER.error("[error][redislock]redis连接、关闭、加锁异常[{}][{}]", ie.getMessage(), ie);
} catch (Exception e) {
LOGGER.error("[error][redislock]redis服务处理总异常[{}][{}]", e.getMessage(), e);
} finally {
//如果机器网络断开,此服务将状态位置为false,forceUnlock强制解锁(如果为unlock则需要等待看门狗30s后才可解锁)
if(lockFlag.get()){
lockFlag.set(false);
lock.forceUnlock();
}
first.set(0);
LOGGER.info("[redislock]报异常,锁key[{}]解除锁定,锁状态位重置[{}]",lockKey,lockFlag);
}
}
@PreDestroy
public void destroy(){
LOGGER.info("[redislock][destroy]本机key[{}]销毁开始...当前是否持有锁[{}]",lockKey,lockFlag);
//设置use为false
setUseFlag();
//关闭redis连接
if(redissonCient!=null){
//解锁处理
RLock lock = redissonCient.getLock(lockKey);
if(lock != null && lock.isLocked()&&lockFlag.get()){
LOGGER.info("[redislock][destroy]本机持有锁,对此key进行解锁处理");
//lock.unlock();
lock.forceUnlock();
}
LOGGER.info("[redislock][destroy]关闭redis连接并销毁客户端");
redissonCient.shutdown();
}
if (thread != null) {
thread.interrupt();
LOGGER.info("[redislock][destroy]服务锁线程销毁完成");
}
}
private synchronized void setUseFlag() {
use=false;
}
/**获取本机是否持有锁***/
public boolean isLockFlag() {
return lockFlag.get();
}
}
redis常见问题分析
如何解决缓存雪崩?
如何解决缓存穿透?
如何保证缓存与数据库双写时一致的问题?
一、缓存雪崩
1.1什么是缓存雪崩?
回顾一下我们为什么要用缓存(Redis):

现在有个问题,如果我们的缓存挂掉了,这意味着我们的全部请求都跑去数据库了。

在前面学习我们都知道Redis不可能把所有的数据都缓存起来(内存昂贵且有限),所以Redis需要对数据设置过期时间,并采用的是惰性删除+定期删除两种策略对过期键删除。Redis对过期键的策略+持久化
如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中。
这就是缓存雪崩:
Redis挂掉了,请求全部走数据库。
对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。
缓存雪崩如果发生了,很可能就把我们的数据库搞垮,导致整个服务瘫痪!
1.2如何解决缓存雪崩?
对于“对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。”这种情况,非常好解决:
解决方法:
1、在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。
2、对于“Redis挂掉了,请求全部走数据库”这种情况,我们可以有以下的思路:
事发前:实现Redis的高可用(
主从架构+Sentinel(哨兵) 或者Redis Cluster(集群)),尽量避免Redis挂掉这种情况发生。
事发中:万一Redis真的挂了,我们可以设置
本地缓存(ehcache)+
限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的)
事发后: redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据 。
事发后: redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据 。
3、设置缓存永不过期
这里的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
还有一个方面,如果需要设置缓存永不过期,那么对于此类缓存来说,一般性不会应用于大量的数据量并发处理,在一定时间上,缓存数量不会过多,例如一些全局性参数,静态参数等公共类不易变更信息。
二、缓存穿透
2.1什么是缓存穿透
比如,我们有一张数据库表,ID都是从1开始的(正数):
但是可能有黑客想把我的数据库搞垮,每次请求的ID都是负数。这会导致我的缓存就没用了,请求全部都找数据库去了,但数据库也没有这个值啊,所以每次都返回空出去。
缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

这就是缓存穿透:
请求的数据在缓存大量不命中,导致请求走数据库。
缓存穿透如果发生了,也可能把我们的数据库搞垮,导致整个服务瘫痪!
2.1如何解决缓存穿透?
解决缓存穿透也有两种方案:
1、由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用
布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就
不让这个请求到数据库层!
当我们从数据库找不到的时候,我们也将这个
空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。
这种情况我们一般会将空对象设置一个较短的过期时间。
缓存与数据库双写一致
3.1对于读操作,流程是这样的
上面讲缓存穿透的时候也提到了:如果从数据库查不到数据则不写入缓存。
一般我们对
读操作的时候有这么一个固定的套路:
如果我们的数据在缓存里边有,那么就直接取缓存的。
如果缓存里没有我们想要的数据,我们会先去查询数据库,然后将
数据库查出来的数据写到缓存中。
最后将数据返回给请求
3.2什么是缓存与数据库双写一致问题?
如果仅仅查询的话,缓存的数据和数据库的数据是没问题的。但是,当我们要更新时候呢?各种情况很可能就造成数据库和缓存的数据不一致了。
这里不一致指的是:数据库的数据跟缓存的数据不一致

从理论上说,只要我们设置了键的过期时间,我们就能保证缓存和数据库的数据最终是一致的。因为只要缓存数据过期了,就会被删除。随后读的时候,因为缓存里没有,就可以查数据库的数据,然后将数据库查出来的数据写入到缓存中。
除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生。
3.3对于更新操作
一般来说,执行更新操作时,我们会有两种选择:
先操作数据库,再操作缓存
先操作缓存,再操作数据库
首先,要明确的是,无论我们选择哪个,我们都希望这两个操作要么同时成功,要么同时失败。所以,这会演变成一个分布式事务的问题。
所以,如果原子性被破坏了,可能会有以下的情况:
操作数据库成功了,操作缓存失败了。
操作缓存成功了,操作数据库失败了。
如果第一步已经失败了,我们直接返回Exception出去就好了,第二步根本不会执行。
3.3.1操作缓存
操作缓存也有两种方案:
更新缓存
删除缓存
一般我们都是采取删除缓存缓存策略的,原因如下:
1、 高并发环境下,无论是先操作数据库还是后操作数据库而言,如果加上更新缓存,那就更加容易导致数据库与缓存数据不一致问题。( 删除缓存直接和简单很多)2、如果每次更新了数据库,都要更新缓存【这里指的是频繁更新的场景, 这会耗费一定的性能】,倒不如 直接删除掉。等再次读取时,缓存里没有,那我到数据库找,在数据库找到再写到缓存里边(体现懒加载)
基于这两点,对于缓存在更新时而言,都是建议执行删除操作!
正常的情况是这样的:
先操作数据库,成功;
再删除缓存,也成功;
如果原子性被破坏了:
第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据。如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。
如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:
1、 缓存刚好失效2、线程A查询数据库,得一个旧值3、线程B将新值写入数据库4、线程B删除缓存5、线程A将查到的旧值写入缓存要达成上述情况,还是说一句概率特别低:
因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
删除缓存失败的解决思路:
将需要删除的key发送到消息队列中自己消费消息,获得需要删除的key不断重试删除操作,直到成功
3.3.3先删除缓存,再更新数据库
正常情况是这样的:先删除缓存,成功;再更新数据库,也成功;
如果原子性被破坏了:
第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的。如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的。
看起来是很美好,但是我们在并发场景下分析一下,就知道还是有问题的了:
线程A删除了缓存线程B查询,发现缓存已不存在线程B去数据库查询得到旧值线程B将旧值写入缓存线程A将新值写入数据库
所以也会导致数据库和缓存不一致的问题。
并发下解决数据库与缓存不一致的思路:
将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化。

3.4对比两种策略
先删除缓存,再更新数据库
在高并发下表现不如意,在原子性被破坏时表现优异
先更新数据库,再删除缓存(Cache Aside Pattern设计模式)
在高并发下表现优异,在原子性被破坏时表现不如意
参考网站: