文章目录
简介
Remote Dictionary Server(Redis)是一个开源的key-value存储系统,采用C语言编写,性能优异,支持网络,数据存储可以基于内存也可以基础持久化的日志,虽然很多地方将Redis称作数据库,但通常Redis被用来构建系统中共享的高速的缓存,当然也可以将其用作数据库,但需要注意处理好因故障而导致数据丢失的问题。
Redis支持多种数据结构,包括字符串(String),字典(Hash),列表(List),集合(Set)和有序集合(Sorted Set),因此我们可以方便地存储多种类型的数据。
Redis安装与启动
安装Redis
安装步骤参见菜鸟教程:
https://www.runoob.com/redis/redis-install.html
需要注意的是,Redis官网并没有提供Windows版本的,因为——我也不知道为啥,反正人家就是不想提供吧~~所以我们在Windows上使用的是微软开源维护的的一个分支版本,需要在微软开源的github网站上获取。
在Linux下Redis可以通过在配置文件中修改配置项,来实现后台进程启动,而在Windows下无法通过配置项实现,需要自己注册Windows服务(如果使用的是Windows安装版,安装时会自动注册一个服务,并默认随机启动,使用的配置文件是redis.windows.service.conf)。
启动Redis Server
Windows下在Redis安装目录中打开命令窗口,执行下述命令即可启动redis server,启动后窗口需要保持开启不可关闭
redis-server redis.windows.conf
启动Redis Client
与启动Redis Server一致,只是启动的程序不同
redis-cli -h <hostname> -p <port> -a <password>
hostname和port分别是是需要连接的redis server的ip与端口,password是在配置中设置的密码,没有可以不管,也可以不加任何参数,这样redis-cli默认连接的是127.0.0.1:6379,客户端连接成功后,即可进行数据操作
Redis常用命令
系统相关命令
命令 | 解析 |
---|---|
flushall | 清除所有存储内容 |
exists [key] | 查询指定key是否存在 |
del [key] | 删除指定的key及其value |
keys [pattern] | 按照pattern给定的规则,筛选符合的key,*指代任意长度字串,?指代任意单个字符 |
字符串(String)
命令 | 解析 |
---|---|
set [key] [vlaue] | 设置指定key的值,没有则会创建 |
get [key] | 获取指定key的值 |
getset [key] [value] | 设置指定key的值,并返回旧的值 |
mset [key1] [value1] [key2] [value2]… | 批量设置key-value对 |
mget [key1] [key2]… | 批量获取key对应的value值 |
setex [key] [seconds] [value] | 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位) |
psetex [key] [milliseconds] [value] | 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位 |
setnx [key] [value] | 只有在 key 不存在时设置 key 的值 |
strlen [key] | 返回 key 所储存的字符串值的长度 |
append [key] [value] | 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾 |
字典(Hash)
命令 | 解析 |
---|---|
hset [key] [f1] [v1] | 将哈希表 key 中的字段 field 的值设为 value |
hget [key] [f1] | 获取存储在哈希表中指定字段的值 |
hgetall [key] | 获取在哈希表中指定 key 的所有字段和值 |
hmset [key] [f1] [v1] [f2] [v2]… | 同时将多个 field-value (域-值)对设置到哈希表key中 |
hmget [key] [f1] [f2]… | 获取所有给定字段的值 |
hsetnx [key] [f] [v] | 只有在字段 field 不存在时,设置哈希表字段的值 |
hkeys [key] | 获取所有哈希表中的字段 |
hlen [key] | 获取哈希表中字段的数量 |
hexists [key] [f] | 查看哈希表 key 中,指定的字段是否存在 |
列表(List)
命令 | 解析 |
---|---|
lpush [key] [v1] [v2]… | 将一个或多个值插入到列表头部(即左边) |
lpushx [key] [v] | 将一个值插入到已存在的列表头部,不支持插入多个值 |
rpush [key] [v1] [v2]… | 将一个或多个值插入到列表尾部(即右边) |
rpushx [key] [v] | 将一个值插入到已存在的列表尾部,不支持插入多个值 |
lrange [key] [start] [stop] | 从左至右获取列表指定范围内的元素 |
lset [key] [index] [value] | 通过索引设置列表元素的值,索引从0开始 |
lrem [key] [count] [value] | 从左边开始,移除count个值为value的元素 |
lpop [key] | 移除列表最左边的一个元素,返回值为移除的元素 |
rpop [key] | 移除列表最右边的一个元素,返回值为移除的元素 |
lindex [key] [index] | 通过索引获取列表中的元素,索引从0开始 |
llen [key] | 获取列表的长度 |
blpop [k1] [k2]… [seconds] | 移出并获取列表左边的的第一个元素, 如果列表没有元素会阻塞列表直 到等待超时或发现可弹出元素为 止,获取到k1,k2…中任一个即可 |
brpop [k1] [k2]… [seconds] | 同上,获取的是右边的第一个元素 |
无序集合(Set)
Set是String类型的无序集合,集合中的元素是唯一的,不能出现重复的元素
命令 | 解析 |
---|---|
sadd [key] [v1] [v2]… | 向集合添加一个或多个成员 |
smembers [key] | 返回集合中的所有成员 |
spop [key] | 移除并返回集合中的一个随机元素 |
sismember [key] [member] | 判断 member 元素是否是集合 key 的成员 |
srem [key] [v1] [v2] | 移除集合中一个或多个元素 |
scard [key] | 获取集合的成员数 |
有序集合(Sorted Set)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
命令 | 解析 |
---|---|
zadd [key] [score1] [member1] [score2] [member2]… | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
zcard [key] | 获取有序集合的成员数 |
zrange [key] [start] [stop] (withscores) | 通过索引区间返回有序集合成指定区间内的成员,分数低的会排列在前,分数相同的按入栈先后 |
zcount [key] [min] [max] | 计算在有序集合中指定区间分数的成员数 |
zrank [key] [member] | 返回有序集合中指定成员的索引 |
zrevrank [key] [member] | 返回有序集合倒序排序时,指定成员的索引 |
zrem [key] [member1] [member2]… | 移除有序集合中的一个或多个成员 |
zremrangebyrank [key] [start] [stop] (withscores) | 移除有序集合中给定的排名区间的所有成员 |
zremrangebyscore [key] [min] [max] (withscores) | 移除有序集合中给定的分数区间的所有成员 |
zscore [key] [member] | 返回有序集中,成员的分数值 |
主从集群
主从复制
什么是主从复制
主从复制,顾名思义,就是主服务和从服务之间的同步复制,主服务的数据与从服务的数据完全一致(正常且完全同步的情况下)。一个主服务可以有多个从服务,一个从服务只能对应一个主服务。
Redis通过简单的配置就可以实现主从复制的架构,在主从复制架构中,只有主服务能够写入数据,而从服务只能被动跟踪主服务的数据变动。
为什么要使用主从复制
通过主从复制,我们可以保证数据的安全性,一旦主服务宕机,从服务器中的数据就可以作为备份立即启用。
另外通过主从复制,我们可以实现数据的读写分离,从而分散服务器压力。
主从复制原理简述
Redis的主从复制是通过同步执行命令来实现的,主服务收到写入的命令后,会将命令记录并严格按照执行顺序发送给所有的从服务,从而实现数据的同步。如果从服务中途宕机,重新上线后,会从中断的位置进行同步,以保证主从服务数据完全一致。
如何实现主从复制
Redis配置主从复制非常简单,主服务不需要做任何改动,按照一下几步,即可建立一个主从复制
-
首先需要准备至少两个Redis程序
-
修改从服务的redis.windows.conf(linux下应为redis.conf),将slaveof配置打开
slaveof <masterip> <masterport>
显而易见,masterip就是主服务的ip,masterport就是主服务的端口
配置完成后,启动主服务与从服务
-
打开主服务的redis-cli,执行命令info replication,应该可以看到如下图的信息
-
打开从服务的redis-cli,执行命令info replication,如果看到下图的信息,则说明主从复制已经建立了
接下来我们可以尝试在从服务中写入数据,因为从服务没有写入权限,会有如下提示:
哨兵
什么是哨兵
哨兵是一个独立运行的进程,正如它的名称,它是用来监控Redis系统的运行状况的,哨兵主要有两个功能:
-
监控系统中的主服务和从服务已经其它哨兵是否运行正常
-
主服务出现故障时自动将从服务转换为主服务,从而保证系统的正常运行
通过哨兵,可以增加Redis系统的可用性。
哨兵的实现原理
哨兵与主从服务的架构
一主多从一哨兵的架构如下:
一主多从多哨兵的架构如下:
哨兵监控的原理
-
哨兵进程启动时会读取配置文件,根据配置文件的内容找到需要监控的主数据库的信息,配置内容如下:
sentinel monitor <mastername> <host> <port> <quorum>
mastername是为主服务起的别名,因为在故障恢复后主服务的地址和端口会发生变化,为了准确获取当前的主服务信息,所以为其起了一个别名。我们可以配置多个主服务,即哨兵可以同时监控多个主从系统。
host和port分别是主服务的ip和端口,这两个数据是会变动的,在经历故障恢复时,哨兵进程会同时将这里的配置修改为新的主服务的ip和端口,从而保证每次启动时能够正确连接到主服务。
哨兵会监控系统中所有的主从服务,但我们只需要在配置中指明主服务即可,哨兵会从主服务那里得到一份从服务的清单,然后和从服务建立连接。
quorum是最低通过的投票数,主要有两个用途:
- 哨兵在确认主服务客观下线时,需要达到quorum数量的其它哨兵认为主服务主观下线,才能最终确认主服务客观下线,并开始故障恢复过程;
- 哨兵在选举领头哨兵时,需要超过半数且超过quorum数量的哨兵同意选举其为领头哨兵,其才能成为领头哨兵。
-
哨兵启动后,会与要监控的主服务建立两条连接,这两个连接的建立方式和普通的Redis客户端一样。这两条连接一条用来订阅主服务的
_sentinel_:hello
频道以获取其它同样监控该主服务的哨兵节点的信息,另一条用来定期向主服务发送INFO等命令来获取主服务本身的信息,这其中就包含该主服务下的从服务列表。和主服务建立连接后,哨兵会执行以下三个操作,并且这三个操作贯穿于哨兵的整个生命周期:
-
每10秒哨兵会向主服务和从服务发送INFO命令
解析: INFO命令可以获得当前数据库的相关信息(包括运行ID、复制信息等),前面说的通过主服务而自动获取从服务信息正式通过INFO命令来实现的。哨兵与主服务建立连接,获取到从服务信息列表后,会与所有的从服务建立同样的两条连接。之后,哨兵会每隔10秒,向已知的所有主从服务发送INFO命令获取信息,并对维护的信息进行更新,例如对新增的从服务进行监控,对因为故障恢复而引起的各服务的角色变化进行更新等。
-
每2秒哨兵会向主服务和从服务的
_sentinel_:hello
频道发送自己的信息解析: 哨兵会订阅其监控的每个服务的
_sentinel_:hello
频道,并向其发送自己的信息,其它哨兵收到信息后,会判断是否是新发现的哨兵,如果是就将其加入到已发现的哨兵列表中,并创建一条到该哨兵的连接用来发送PING命令。简而言之,哨兵们通过_sentinel_:hello
频道,实现了哨兵之间的相互发现。 -
每1秒哨兵会向主从服务和其它哨兵节点发送PING命令
解析: 哨兵会每隔一段时间向其监控的主从服务和其它哨兵节点发送PING命令,从而确定对方是否正常运行,这个时间与down-after-milliseconds有关,当down-after-milliseconds的值小于1秒,哨兵会每隔down-after-milliseconds指定的时间发送一次PING命令,如果大于1秒,哨兵则每隔1秒发送一次PING命令。
down-after-milliseconds参数的设置如下:
sentinel down-after-milliseconds <mastername> <milliseconds>
哨兵确认一个服务是正常运行还是下线,有主观下线与客观下线两个步骤。
当超过down-after-milliseconds参数指定的时间后被PING的节点仍未回复,哨兵就认为其主观下线(subjectively down)。而如果该节点是主服务,哨兵就会进一步判断是否需要对其进行故障恢复,哨兵会向其它哨兵节点发送SENTINEL is-master-down-by-addr命令来询问他们是否也认为该主服务主观下线,如果达到指定数量(即前面配置中的quorum参数),哨兵会认为其客观下线(objectively down),需要对该主服务进行故障恢复。
因为需要保证同一时间只有一个哨兵节点来执行故障恢复,所以Redis采用了选举领头哨兵的机制,由领头哨兵来执行故障恢复任务,选举领头哨兵的过程使用了Raft算法,具体过程如下:
- 发现主服务客观下线的哨兵(后文称作A)向每个哨兵发送命令,要求对方选举自己成为领头哨兵;
- 如果目标哨兵没有选举过其它哨兵,则会同意选举A为领头哨兵;
- 如果A发现有半数且超过quorum参数值的哨兵选举自己为领头哨兵,那么A成为领头哨兵;
- 如果有多个哨兵同时参选,那么可能出现没有任何节当选的情况,这时所有参选哨兵将等待重新发起参选请求,直到最终选举出领头哨兵。
选举出领头哨兵后,领头哨兵将开始执行故障恢复任务,具体过程如下:
-
领头哨兵从停止服务的主服务的从服务中挑选一个来充当新的主服务,选择依据如下:
- 所有在线的从服务中,选择优先级最高的从服务。优先级通过slave-priority参数设置;
- 如果有多个最高优先级的从服务,那么选择复制命令的偏移量来选择,越大越优先(即数据越完整);
- 如果上述条件均一致,则选择运行ID最小的从服务。
-
选出新的主服务后,领头哨兵将向该从服务发送slaveof no one命令,使其升级为主服务。然后领头哨兵向其它从服务发送slaveof命令来使这些从服务成为新主服务的从服务;
-
更新内部维护的记录,并且会将旧主服务更新为新主服务的从服务,待其重新上线时对其发送slaveof命令,然后以从服务的身份继续提供服务。
-
如何实现哨兵
我们可以通过以下几个简单步骤实现一个单哨兵的架构:
-
首先我们按照上面的方式构建一个主从复制的架构
-
然后我们再准备一个Redis程序,用来作为哨兵
-
在Redis目录下创建一个sentinel.conf配置文件,用来配置哨兵的属性,配置的内容如下:
sentinel monitor <mastername> <host> <port> <quorum>
mastername是为主服务起的别名,host和port是主服务的ip和端口,quorum是最低通过票数,在主服务下线后选举新的领头哨兵时需要用到。
在配置中只需要指明主服务即可,哨兵可以从主服务中拿到从服务的清单,并建立监听。
-
-
启动哨兵服务
redis-server sentinel.conf --sentinel
可以看到如下图的提示:
至此我们的一二从一哨兵的架构就搭建起来了,我们可以来尝试验证哨兵是否生效。
-
关闭一个从服务,观察哨兵的行为
可以看到哨兵中有如上图的输出,说明哨兵感知到了从服务下线。
-
重新启动刚才关闭的从服务,观察哨兵的行为
哨兵感知到了从服务的重新上线
-
关闭一个主服务,观察哨兵的行为
可以看到如图的一系列输出,哨兵感知到了主服务的下线,并进行故障恢复,最终恢复的结果是6381成为了主服务,我们看下6381的信息:
6381已经成为当前的主服务了,成功完成了一次故障恢复。
-
重启6380,即之前的主服务后,6380将成为6381的从服务,并且这不是暂时的,因为在故障恢复时,哨兵服务会修改Redis服务的配置文件,以后启动Redis系统,主服务都将是6381,除非人为干预或者经历了另外的故障恢复
至此我们的验证就结束了,验证结果说明了我们的哨兵已经正常工作,能够正常监听主从服务,并进行故障恢复
-
多哨兵与单哨兵配置一致,只是哨兵之间会互相监听,同时选举出一个领头哨兵,在执行故障恢复等任务时,都是领头哨兵在进行,当领头哨兵故障后,又会重新选举出新的领头哨兵,多哨兵主要是为了防止哨兵进程故障导致监控失效,提高系统可用性。
在Spring Boot中使用Redis
学习源码地址:https://gitee.com/imdongrui/study-repo.git下redis目录中
使用单机Redis服务
-
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在application中配置Redis
spring: redis: host: 127.0.0.1 port: 6379 #password: #密码,没有可不设置 pool: max-active: 8 #最大连接数,负值表示没有限制 max-wait: -1 #最大阻塞等待时间,-1表示没有限制 max-idle: 8 #最大空闲连接 min-idle: 1 #最小空闲连接 timeout: 60000 #连接超时时间,单位毫秒
-
通过javabean配置RedisTemplate,主要设置序列化工具
下图是Spring Boot提供的StringRedisTemplate,主要关注其中的序列化工具设置
Spring Boot为我们提供了一些序列化工具,常见的有JdkSerializationRedisSerializer、StringRedisSerializer、Jackson2JsonRedisSerializer、GenericJackson2JsonRedisSerializer等,可以根据实际业务选择合适的序列化工具进行设置
JdkSerializationRedisSerializer:使用的jdk的序列化工具,主要用于序列化java对象,被序列化的对象必须实现Serializable接口。
StringRedisSerializer:简单的字符串序列化工具,key的序列化工具多采用此。
Jackson2JsonRedisSerializer:将Object对象序列化为json字符串,在序列化时不会记录属性类型,所以在有泛型的情况下,可能会反序列化失败或者不是实际想要的类型。
GenericJackson2JsonRedisSerializer:同样是将Object对象序列化为json字符串,但在序列化时会记录属性类型,所以在反序列化时可以准确地转化为实际的类型,但相比于Jackson2JsonRedisSerializer效率较低。
可使用如下方式配置
package com.dongrui.study.redis.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){ StringRedisTemplate template = new StringRedisTemplate(factory); setSerializer(template); template.afterPropertiesSet(); return template; } /** * 为RedisTemplate设置序列化工具 */ private void setSerializer(StringRedisTemplate template){ //初始化一个,Jackson2JsonRedisSerializer 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); //设置value的序列化工具 template.setValueSerializer(jackson2JsonRedisSerializer); //设置hash value的序列化工具,hash key仍为StringRedisSerializer template.setHashValueSerializer(jackson2JsonRedisSerializer); } }
-
然后就可以开始愉快地使用Redis了~
public class StudyService { @Autowired private RedisTemplate redisTemplate; public void putStudent(String name, int age) { redisTemplate.opsForValue().set(name, new Student(name, age)); } public Student getStudent(String name) { return (Student) redisTemplate.opsForValue().get(name); } public void putStudentMap() { Student marry = new Student("marry", 18); Student bob = new Student("bob", 19); redisTemplate.opsForHash().putAll("class-1", new HashMap() {{ put("marry", marry); put("bob", bob); }}); } }
使用基于哨兵的Redis集群
要用哨兵也很简单,只需要增加哨兵的配置即可
spring:
redis:
pool:
max-active: 8 #最大连接数,负值表示没有限制
max-wait: -1 #最大阻塞等待时间,-1表示没有限制
max-idle: 8 #最大空闲连接
min-idle: 1 #最小空闲连接
timeout: 60000 #连接超时时间,单位毫秒
sentinel:
master: mymaster #监控的主服务名,即前面我们在sentinel.conf文件里配置过的
nodes: 127.0.0.1:26379 #多个哨兵节点使用半角逗号分割
需要注意的是,因为哨兵进行故障恢复需要一些时间,所以spring.redis.timeout需要设置大一些,否则还没有完成故障恢复,就已经超时了。
Redis原理知识
持久化原理
Redis提供了RDB和AOF两种持久化方式,RDB默认开启,AOF默认关闭。
RDB原理简析
RDB方式的持久化是通过快照来实现的,在下列情况时Redis会对当前数据进行快照:
- 根据配置规则进行自动快照
- 用户执行save或bgsave命令
- 执行flushall命令
- 执行复制(replication)时
Redis默认将快照文件存储在Redis当前进程的工作目录中的dump.rdb中,快照的过程如下:
- Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
- 父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
- 当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此一次快照的过程完成。
通过此过程可以看到,Redis不会修改RDB文件,只是使用新文件替换旧文件,所以RDB文件中的数据始终是完整的,我们可以通过定时备份RDB文件来对Redis数据进行备份。
Redis启动后会读取RDB文件中的数据,将这些数据从硬盘中载入到内存中。
通过RDB方式实现持久化,一旦Redis异常退出,会丢失最后一次快照之后变更的所有数据,需要酌情使用。
AOF原理简析
AOF(append only file)方式是基于日志文件的持久化方式,默认情况下关闭的,可以通过配置参数启用
appendonly yes
开启AOF持久化后,Redis每执行一条设计更改的命令,都会将该命令写入硬盘中的AOF文件,AOF文件保存的位置和RDB相同,默认文件名为appendonly.aof。尽管每次执行更改命令Redis都会向AOF文件写入命令记录,但是由于操作系统的缓存机制,数据并没有真正地写入到硬盘,而是进入了硬盘的缓存,默认情况下系统每隔30秒回执行一次同步操作,将硬盘缓存中的数据真正写入到硬盘,此时如果出现断电等故障,仍然会导致数据的丢失,这个同步时间可以通过Redis的appendfsync参数进行设置。
#everysec-每秒执行一次同步,always-每条命令都执行同步,no-由操作系统控制
#默认设置为everysec,在安全性和速度上均可兼顾
appendfsync everysec
Redis会自动对文件中的命令进行优化合并以及重写,尽量保持文件不至于过大。
在启动时,Redis会逐个执行AOF文件中的命令,将硬盘中的数据载入到内存中,载入速度较RDB会慢一些。
Redis允许同时开启RDB和AOF,此时重启Redis时会使用AOF文件来载入数据,因为AOF方式的持久化相对较安全一些。