Redis的理解与应用
1.Redis的介绍;
Redis是用c语言编写的,是一个单线程,高性能的(key/value)内存数据库,基于内存运行并支持持久化的nosql数据库,主要是用来做缓存,但不仅仅只能做缓存,比如:redis的计数器生成分布式唯一主键,redis实现分布式锁,队列,会话缓存。
2.Redis的数据类型及API操作:
1)string类型:string是redis最基本的类型,一个key对应一个value,string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 ,一个redis中字符串value最多可以是512M。
set key value 设置key value
get key 查看当前key的值
del key 删除key
append key value 如果key存在,则在指定的key末尾添加,如果key存在则类似set
2)list类型:list是一个字符串链表,left、right都可以插入添加;如果键不存在,创建新的链表;如果键已存在,新增内容;如果值全移除,对应的键也就消失了。链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。它的底层实际是个链表。
lpush key value1 value2 将一个或多个值加入到列表头部
rpush key value1 value2 将一个或多个值加入到列表底部
lpop key 移出并获取列表第一个元素
rpop key 移出并获取列表最后一个元素
llen 获取列表长度
3)set类型:set是string类型的无序,不能重复的集合。
sadd key value1 value2 向集合中添加一个或多个成员
smembers key 返回集合中所有成员
scard key 获取集合里面的元素个数
srem key value 删除集合中指定元素
sdiff key1 key2 在第一个set里面而不在后面任何一个set里面的项(差集)
sinter key1 key2 在第一个set和第二个set中都有的 (交集)
sunion key1 key2 两个集合所有元素(并集)
4)hash类型:hash 是一个键值对集合,是一个string类型的field和value的映射表,hash特别适合用于存储对象。kv模式不变,但v是一个键值对,类似Java里面的Map<String,Object>
hset key (key value) 向hash表中添加一个元素
hget key key 向hash表中获取一个元素
hgetall key 获取在hash列表中指定key的所有字段和值
hdel key key1 key2 删除一个或多个hash字段
hlen key 获取hash表中字段数量
hexits key key 查看hash表中,指定key(字段)是否存在
hkeys key 获取指定hash表中所有key(字段)
hvals key 获取指定hash表中所有value(值)
5)zset类型:zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
zadd key score 值 score 值 向集合中添加一个或多个成员
zrange key 0 -1 表示所有 返回指定集合中所有value
zrem key score某个对应值(value),可以是多个值 删除元素
zcount key 开始score 结束score 获取分数区间内元素个数
3.Redis的持久化机制
Redis持久化就是在指定的时间间隔内,将内存当中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存。
Redis提供两种方式进行持久化:
1)RDB持久化,也是默认的持久化方式
2)AOF(append only file)持久化
RDB持久化:
RDB持久化原理是redis会单独创建(fork)一个与当前线程一模一样的子线程来进行持久化,这个子线程的所有数据(变量。环境变量,程序程序计数器等)都和原线程一模一样,会先将数据写入到一个临时文件中,待持久化结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程不进行任何的io操作,这就确保了极高的性能。
什么时候触发RDB持久化机制:
1.在执行shutdown命令时,如果没有开启AOF,这时就会触发RDB持久化。
2.在执行save命令或者bgsave命令时, save:是只管保存,其他不管,全部阻塞; bgsave: redis会在后台异步进行快照操作,同时可以响应客户端的请求。
AOF持久化:
AOF持久化的原理是将Reids的操作日志以追加的方式写入文件,读操作是不记录的。
什么时候触发AOF持久化机制:
1.在配置文件中进行AOF的配置即可。
no:表示等操作系统进行数据缓存同步到磁盘(快,持久化没保证)
always:同步持久化,每次发生数据变更时,立即记录到磁盘(慢,安全)
everysec:表示每秒同步一次(默认值,很快,但可能会丢失一秒以内的数据)
AOF重写机制:
当AOF文件增长到一定大小的时候Redis能够调用 bgrewriteaof对日志文件进行重写 。当AOF文件大小的增长率大于该配置项时自动开启重写(这里指超过原大小的100%)。
Redis4.0后的混合持久化机制:
Redis 4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,5.0之后默认开启。
混合持久化是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入AOF文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据。
优点:混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。
缺点:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差
总结:
Redis提供了RDB持久化方案,为什么还要AOF?优化数据丢失问题,rdb会丢失最后一次快照后的数据,aof丢失不会超过2秒的数据。
如果RDB和AOF同时存在,听谁的?以AOF为主。
RDB和AOF相比较有什么优劣?RDB 适合大规模的数据恢复,对数据完整性和一致性不高 , 在一定间隔时间做一次备份,如果Redis意外down机的话,就会丢失最后一次快照后的所有操作,而AOF 根据配置项而定。
4.Redis的主从复制:
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,mester以写为主,slaver以读为主。
使用命令 SLAVEOF 动态指定主从关系 ,如果设置了密码,关联后使用 config set masterauth 密码。
总结:
一个master可以有多个Slave
一个slave只能有一个master
数据流向是单向的,只能从主到从
5.Redis的哨兵模式:
哨兵的核心功能是主节点的自动故障转移。通俗来讲哨兵模式的出现是就是为了解决我们主从复制模式中需要我们人为操作的东西变为自动版,并且它比人为要更及时
哨兵主要功能(做了哪些事)
1)监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
2)自动故障转移(Automatic Failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
3)配置提供者(Configuration Provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
4)通知(Notification):哨兵可以将故障转移的结果发送给客户端。
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。
哨兵模式的原理:
1)主观下线:在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线;与主观下线相对应的是客观下线。
2)客观下线:哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。
需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。
定时任务:
每个哨兵节点维护了3个定时任务。定时任务的功能分别如下:
1)每10秒通过向主从节点发送info命令获取最新的主从结构;发现slave节点 确定主从关系
2)每2秒通过发布订阅功能获取其他哨兵节点的信息;SUBSCRIBE c2 PUBLISH c2 hello-redis
3)每1秒通过向其他节点发送ping命令进行心跳检测,判断是否下线(monitor)。
哨兵模式选举领导者:
当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。
监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。
故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为3个步骤:
1)在从节点中选择新的主节点:选择的原则是,
1.首先过滤掉不健康的从节点;
2.然后选择优先级最高的从节点(由replica-priority指定);如果优先级无法区分,
3.则选择复制偏移量最大的从节点;如果仍无法区分,
4.则选择runid最小的从节点。
2)更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。
3)将已经下线的主节点(即6379)保持关注,当6379从新上线后设置为新的主节点的从节点
总结:
在主从复制的基础上,哨兵引入了主节点的自动故障转移,进一步提高了Redis的高可用性;但是哨兵的缺陷同样很明显:哨兵无法对从节点进行自动故障转移,在读写分离场景下,从节点故障会导致读服务不可用,需要我们对从节点做额外的监控、切换操作。此外,哨兵仍然没有解决写操作无法负载均衡、及存储能力受到单机限制的问题
6.Redis cluster高可用集群:
Redis cluster集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis cluster集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到1000节点。redis cluster集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
Redis cluster集群的优点:
通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。
7.Redis缓存的使用问题:
1)缓存粒度的控制
缓存粒度问题就是我们在使用缓存时,是将所有数据缓存还是缓存部分数据?
| 数据类型 | 通用性 | 空间占用(内存空间+网络码率) | 代码维护 |
| :--------: | :------: | :---------------------------------: | :--------: |
| 全部数据 | 高 | 大 | 简单 |
| 部分数据 | 低 | 小 | 较为复杂 |
缓存粒度问题是一个容易被忽视的问题,如果使用不当,可能会造成很多无用空间的浪费,可能会造成网络带宽的浪费,可能会造成代码通用性较差等情况,必须学会综合数据通用性、空间占用比、代码维护性 三点评估取舍因素权衡使用。
2)缓存穿透问题
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,并且出于容错考虑, 如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
缓存穿透的危害:对底层数据源压力过大,有些底层数据源不具备高并发性。 例如mysql一般来说单台能够扛1000-QPS就已经很不错了
缓存穿透的解决方案:缓存空对象,如果第一次去数据库查询没有查到数据,就设置成一个具有过期时间的空值,防止短时间多次去数据库查询。
3)缓存击穿问题
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
我们知道,使用缓存,如果获取不到,才会去数据库里获取。但是如果是热点 key,访问量非常的大,数据库在重建缓存的时候,会出现很多线程同时重建的情况。因为高并发导致的大量热点的 key 在重建还没完成的时候,不断被重建缓存的过程,由于大量线程都去做重建缓存工作,导致服务器拖慢的情况。
缓存击穿解决方案:
1.互斥锁:第一次获取缓存的时候,加一个锁,然后查询数据库,接着是重建缓存。这个时候,另外一个请求又过来获取缓存,发现有个锁,这个时候就去等待,之后都是一次等待的过程,直到重建完成以后,锁解除后再次获取缓存命中。
public String getKey(String key){
String value = redis.get(key);
if(value == null){
String mutexKey = "mutex:key:"+key; //设置互斥锁的key
if(redis.set(mutexKey,"1","ex 180","nx")){ //给这个key上一把锁,ex表示只有一个线程能执行,过期时间为180秒
value = db.get(key);
redis.set(key,value);
redis.delete(mutexKety);
}else{
// 其他的线程休息100毫秒后重试
Thread.sleep(100);
getKey(key);
}
}
return value;
}
互斥锁的优点是思路非常简单,具有一致性,但是互斥锁也有一定的问题,就是大量线程在等待的问题。存在死锁的可能性。
4)缓存雪崩问题
缓存雪崩是指机器宕机或在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
4:如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。