Redis特性
-
速度快
数据存放在内存(最主要原因)
C语言实现
单线程架构,预防了多线程可能产生的竞争问题
-
基于键值对的数据结构服务器
主要数据结构:字符串、哈希、列表、集合、有序列表
演变出的数据结构:位图(Bitmaps)、HyperLogLog、GEO(地理信息定位)
-
丰富的功能
键过期功能,可以用来实现缓存
发布订阅功能,可以用来实现消息系统
支持Lua脚本,可以利用Lua创造出新的Redis命令
事务,能在一定程度上保证事务特性
流水线(Pipeline)功能,客户端可以将一批命令一次性传到Redis,减少网络开销
-
简单稳定
源码很少,早期只有2万行,添加集群特性之后增至5万行
单线程模型,不仅使得Redis服务端处理模型变得简单,也使得客户端开发变得简单
不需要依赖操作系统的类库,自己实现了事件处理的相关功能
-
客户端语言多
-
持久化
AOF、RDB
-
主从复制
-
高可用和分布式
使用场景
可以做什么:
缓存(键过期时间设置)
排行榜系统(列表和有序列表)
计数器应用(Redis天然支持计数功能)
社交网络,赞/踩、粉丝、共同好友/喜好、推送(Redis数据结构比较容易实现)
消息队列系统(发布订阅功能和阻塞队列功能)
不可以做什么:
由于数据是存放在内存中的,所以把冷数据放到Redis中是对内存的浪费
为什么单线程还能这么快
纯内存访问,内村的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础
非阻塞IO,Redis使用epoll作为IO多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络IO上浪费过多时间

单线程避免了线程切换和竞态产生的消耗。
Jedis基本操作
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
@Slf4j
public class RedisService {
public static void main(String[] args) {
Jedis jedis = null;
try {
// jedis = createJedis("47.93.59.200", 6379);
jedis = createJedisByPool("47.93.59.200", 6379);
basicOperation(jedis);
}catch (Exception e){
log.error("连接失败");
}finally {
if(!Objects.isNull(jedis)){
jedis.close();
}
}
}
/**
* 直接创建Jedis对象
* @param host
* @param port
* @return
*/
static Jedis createJedis(String host,int port){
return new Jedis(host,port);
}
/**
* 连接池创建Jedis对象
* @param host
* @param port
* @return
*/
static Jedis createJedisByPool(String host,int port){
GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
JedisPool jedisPool = new JedisPool(poolConfig, host, port);
return jedisPool.getResource();
}
/**
* 基本操作
* @param jedis
*/
static void basicOperation(Jedis jedis){
//String
String set = jedis.set("hello", "world");
String hello = jedis.get("hello");
Long counter = jedis.incr("counter");
System.out.println(set+"->"+hello+"->"+counter);
//hash
jedis.hset("user:1","name","zhangsan");
jedis.hset("user:1","age","18");
Map<String, String> stringStringMap = jedis.hgetAll("user:1");
System.out.println(JSONObject.toJSONString(stringStringMap));
//list
jedis.rpush("mylist","1","2","3");
List<String> mylist = jedis.lrange("mylist", 0, -1);
System.out.println(JSONObject.toJSONString(mylist));
//set
jedis.sadd("myset","a","b","a");
Set<String> myset = jedis.smembers("myset");
System.out.println(JSONObject.toJSONString(myset));
//zset
jedis.zadd("myzset",99,"zhangsan");
jedis.zadd("myzset",33,"lisi");
jedis.zadd("myzset",66,"wangwu");
Set<Tuple> myzset = jedis.zrangeWithScores("myzset", 0, -1);
System.out.println(JSONObject.toJSONString(myzset));
Order order = new Order("1", "测试序列化订单", "1");
String jsonString = JSONObject.toJSONString(order);
jedis.set("order:1",jsonString);
String s = jedis.get("order:1");
Order parseObject = JSONObject.parseObject(s, Order.class);
System.out.println(parseObject);
}
/**
* pipeline批量删除
* @param jedis
* @param keys
*/
static void pipelineMDel(Jedis jedis,List<String> keys){
Pipeline pipeline = jedis.pipelined();
for (String key : keys) {
pipeline.del(key);
}
pipeline.sync();
}
}
持久化
-
RDB
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
手动触发
save命令:
阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
bgsave命令:
Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
自动触发
- 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
- 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从从节点。
- 执行debug reload命令重新加载Redis时,也会自动触发save操作。
- 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
RDB文件的处理
保存
RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
压缩
Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression{ yes|no} 动态修改。
RDB的优缺点
优点
- RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如HDFS),用于灾难恢复
- Redis加载RDB恢复数据远远快于AOF的方式
缺点
- RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高
- RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题
-
AOF
AOF(append only file)持久化: 以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
使用AOF
开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)

- 所有的写入命令会追加到aof_buf(缓冲区)中
- AOF缓冲区根据对应的策略(由参数appendfsync控制 )向硬盘做同步操作
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
- 当Redis服务器重启时,可以加载AOF文件进行数据恢复
复制
默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能从主节点复制到从节点。
建立复制:
配置文件加入slaveof {masterHost} {masterPort} ,也可直接使用命令:slaveof {masterHost} {masterPort} 指定主节点。
断开复制:
执行命令 slaveof no one
切换主节点
注意:切主后从节点会清空之前所有的数据
执行命令 slaveof {newMasterIp} {newMasterPort}
一主一从
当应用写命令并发量较高且需要持久化时,可以只在从节点开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的,当主节点关闭持久化功能时,如果主节点脱机要避免自动重启操作,因为主节点之前没有开启持久化功能自动重启后数据集为空,这是从节点如果继续复制主节点会导致从节点数据也被清空的情况,丧失持久化的意义。安全的做法是在从节点上执行salveof no one 断开与主节点的复制关系,再重启主节点从而避免这一问题。
一主多从
可以实现读写分离。
对读占比较大的场景,可以把读命令发送到从节点来分担压力。防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。
对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络宽带,同时也加重了主节点的负载影响服务器稳定性。此时应该使用树状主从结构
树状主从结构
树状主从结构使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。
复制过程
从节点执行slaveof命令后,复制过程便开始运作:
- 保存主节点信息。
- 从节点内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接(socket套接字 ),如果从节点无法创建连接,定时任务会无限重试知道连接成功或者执行salveof no one 取消复制。
- 发送ping命令,检测主从之间网络套接字是否可用,检测主节点当前是否可接收处理命令。如果ping失败,从节点会断开复制连接,下次定时任务会发起重连。
- 权限验证。如果主节点设置了requirepass参数,则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证;如果失败,从节点重新发起复制流程。
- 同步数据集。对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作时耗时最长的步骤。
- 命令持续复制。主节点会持续把写命令发送给从节点,保证主从数据一致性。
Redis Sentinel——Redis的高可用实现方案
主从复制存在的问题:
- 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其它从节点去复制新的节点,整个过程都需要人工干预。
- 主节点的写能力受到单机的限制
- 主节点的存储能力受到单机的限制
Redis Sentinel的高可用性
当主节点出现故障时,Redis Sentinel能够自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。

Redis Sentinel 具有一下几个功能:
监控:sentinel节点会定期检测Redis数据节点、其余sentinel节点是否可达
通知:sentinel节点会将故障转移的结果通知给应用方
主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点 信息
为什么Redis Sentinel包含若干个Sentinel节点?
- 对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判
- 这样即便个别Sentinel节点不可用,整个Sentinel节点集合依然是健壮的
注:sentinel节点本身也是独立的Redis节点,只不过它们有一些特殊,它们不存储数据,只支持部分命令。
部署Sentinel节点
redis数据节点的部署不用特别处理
redis-sentinel-26379.conf
port 26379 #默认端口26379
daemonize yes
logfile "26379.log"
dir /opt/soft/redis/data
sentinel monitor mymaster 127.0.0.1 6379 2 #监控127.0.0.1 6379这个主节点,2代表主节点失败至少需要2 个sentinel节点同意,mymaster是主节点的组名
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
Java操作Redis Sentinel
Sentinel节点集合具备了监控、通知、自动故障转移、配置提供者若干功能,也就是说实际上最了解主节点信息的就是Sentinel节点集合,而各个主节点可以通过<master-name>进行标识,如果需要正确地连接Redis Sentinel,必须有Sentinel节点集合和masterName两个参数。
下面是一个简单例子。
HashSet<String> sentinels = new HashSet<>();
sentinels.add("47.93.59.200:26379");
sentinels.add("47.93.59.200:26380");
sentinels.add("47.93.59.200:26381");
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("mymaster",sentinels);
HostAndPort currentHostMaster = jedisSentinelPool.getCurrentHostMaster();
System.out.println(currentHostMaster); //输出:47.93.59.200:6379
try (Jedis jedis = jedisSentinelPool.getResource()) {
System.out.println(jedis.get("hello"));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
Jedis针对Redis Sentinel给出了一个JedisSentinelPool,Jedis给出了很多构造方法,其中最全的如下所示:
public JedisSentinelPool(
String masterName, //主节点名
Set<String> sentinels, //sentinel节点集合
GenericObjectPoolConfig poolConfig, //common-pool连接池配置
int connectionTimeout, //连接超时
int soTimeout, //读写超时
String password, //主节点密码
int database, //当前数据库索引
String clientName){//客户端名
//省略赋值代码......
HostAndPort master = this.initSentinels(sentinels, masterName);//初始化函数,下面介绍
this.initPool(master);
}
构造方法中重要的初始化函数initSentinels(Set<String> sentinels, String masterName),参数为sentinel节点集合和masterName,用来获取指定主节点的ip地址和端口号。下面来分析一下
private HostAndPort initSentinels(Set<String> sentinels, String masterName) {
//主节点ip和端口号
HostAndPort master = null;
//sentinel是否可用
boolean sentinelAvailable = false;
this.log.info("Trying to find master from available Sentinels...");
Iterator var5 = sentinels.iterator();
String sentinel;
HostAndPort hap;
//遍历所有sentinel节点
while(var5.hasNext()) {
sentinel = (String)var5.next();
//分割sentinel节点的ip和端口号
hap = this.toHostAndPort(Arrays.asList(sentinel.split(":")));
this.log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
//连接sentinel节点
jedis = new Jedis(hap.getHost(), hap.getPort());
//使用“sentinel get-master-addr-by-name masterName”API获取主节点信息
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
sentinelAvailable = true;
if (masterAddr != null && masterAddr.size() == 2) {
//解析主节点的ip与端口号
master = this.toHostAndPort(masterAddr);
this.log.fine("Found Redis master at " + master);
break;
}
this.log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap + ".");
} catch (JedisException var13) {
this.log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + var13 + ". Trying next one.");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
//如果没有找到主节点直接抛异常
if (master == null) {
if (sentinelAvailable) {
throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");
} else {
throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");
}
} else {
this.log.info("Redis master running at " + master + ", starting Sentinel listeners...");
var5 = sentinels.iterator();
//为每个sentinel节点开启主节点switch的监控线程
while(var5.hasNext()) {
sentinel = (String)var5.next();
hap = this.toHostAndPort(Arrays.asList(sentinel.split(":")));
JedisSentinelPool.MasterListener masterListener = new JedisSentinelPool.MasterListener(masterName, hap.getHost(), hap.getPort());
masterListener.setDaemon(true);
this.masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
}
大概步骤为:
- 遍历Sentinel节点集合,找到一个可用的sentinel节点,如果找不到就从sentinel节点集合中去找下一个,如果都找不到直接抛异常为客户端。
- 找到一个可用的sentinel节点,执行sentinelGetMasterAddrByName(masterName),找到对应主节点信息。
- 为每一个sentinel节点单独启用一个线程,利用Redis的发布订阅功能,每个线程订阅Sentinel节点上切换master的相关频道“+switch-master”。
下面是MasterListener的核心监听代码,代码中比较重要的部分就是订阅sentinel节点的+switch-master频道,它就是Redis Sentinel在结束对主节点故障转移后发布切换主节点的消息,Sentinel节点基本将故障转移的各个阶段发生的行为都通过这中发布订阅的形式对外提供,这里我们比较关心的是+switch-master这个频道。
//客户端订阅sentinel节点上“+switch-master”(切换主节点)频道
this.j.subscribe(new JedisPubSub() {
public void onMessage(String channel, String message) {
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3) {
//判断是否为当前的masterName
if (MasterListener.this.masterName.equals(switchMasterMsg[0])) {
// 发现当前masterName发生switch,使用initPool重新初始化连接池
JedisSentinelPool.this.initPool(JedisSentinelPool.this.toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
}
}
}, new String[]{"+switch-master"});
实现原理
三个定时监控任务
- 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构,当有新的从节点加入时都可以立刻感知出来,节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息。
- 每隔2秒,每个Sentinel节点会向Redis数据节点的
__sentinel__:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,同时每个Sentinel节点也会订阅该频道,来了解其它Sentinel节点以及它们对主节点的判断。一方面可以了解其它Sentinel节点信息,另一方面Sentinel节点之间交换主节点状态,作为后面客观下线以及领导者选举的依据。 - 每隔1秒,每隔Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。实现对每个节点的监控,这个定时任务是节点失败判断的重要依据。
主观下线和客观下线
上面第三个定时任务,发送ping命令做心跳检测,当这个节点超过down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。主观下线是当前Sentinel节点的一家之言,存在误判的可能。当Sentinel主观下线的节点是主节点时,该Sentinel节点会向其它Sentinel节点询问对主节点的判断,当超过<quorum>个数,Sentinel节点认为主节点确实有问题,这是该Sentinel节点会做出客观下线的决定,也就是大部分Sentinel节点都对主节点的下线做了同意的判定,那么这个判定就是客观的。
领导者Sentinel节点选举
假如Sentinel节点对于主节点已经做了客观下线,Sentinel节点之间会选出一个Sentinel节点作为领导者进行故障转移工作。Redis使用了Raft算法实现领导者选举,大致思路:
- 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,回想其它Sentinel节点发送
sentinel is-master-down-by-addr命令,要求将自己设置为领导者。 - 收到命令的Sentinel节点,如果没有同意过其它Sentinel节点的
sentinel is-master-down-by-addr命令 ,将同意该请求,否则拒绝,每个Sentinel节点只有一票。 - 如果该Sentinel节点发现自己的票数已经大于等于max(quorum,num(sentinels)/2+1),那么它将成为领导者
- 如果此过程没有选举出领导者,将进入下一次选举。
故障转移
领导者选举出的Sentinel节点负责故障转移,具体步骤如下:
- 在从节点列表中选出一个节点作为新的主节点
- Sentinel领导者节点会对第一步选出来的从节点执行
slaveof no one命令让其成为主节点。 - Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新节点的从节点
- Sentinel节点集合会将原来的主节点更新为从节点,并保持对其关注,当其恢复后命令它去复制新的主节点
集群
Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。
Redis数据分区
Redis Cluster采用虚拟槽分区,虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽。所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。

Redis虚拟槽分区的特点:
- 解耦数据与节点之间的关系,简化了节点扩容和收缩难度
- 节点自身维护槽的映射关系,不需要客户端或者代理服务器维护槽分区元数据
- 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景
集群功能限制
- key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、met等操作可能存在与多个节点上因此不被支持
- key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
- key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点
- 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0
- 复制结构只支持一层,从节点只能复制主节点,不能嵌套树状复制结构
