Redis由来
菜鸟教程:https://www.runoob.com/redis/redis-sorted-sets.html
1、定义
- Remote Dictionary Server(Redis) 是一个由Salvatore
Sanfilippo写的key-value存储系统。 Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 - 它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
- 单线程:因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,如果是多线程来回上下文切换比较耗时,还不如单线程,那就顺理成章地采用单线程的方案了。
- 单线程支持高并发:redis作为单进程模型的程序,为了充分利用多核CPU,常常在一台server上会启动多个实例。而为了减少切换的开销,有必要为每个实例指定其所运行的CPU。
- redis 的瓶颈在网络上。
1.1 缓存中间件 Memcache和Redis
-
memcache:
-
Redis:
理论值:10W+QPS
1)Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
2)集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集群模式的,比memcached来说要更好。
3)性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。
4)内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
1.2 为啥快: IO:多路复用
- I/O多路复用:提高redis多路复用。
- selector:监听可以读写的文件,并返回这些文件描述符,就可以读写文件了 。
- 更好的I/O
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qh8lzyH9-1611902674929)(C:\Users\Administrator\Desktop\NOTES\Redis.assets\image-20210121225500735.png)]
2、数据类型
- Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
1、语法 set name “value”
set name "redis"
set name "memcache"
get name (返回"memcache")
2.1String
redis 127.0.0.1:6379> SET runoobkey redis
OK
redis 127.0.0.1:6379> GET runoobkey
"redis"
2.2 Hash
- Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
127.0.0.1:6379> HMSET runoobkey name "redis tutorial" description "redis basic commands for caching" likes 20 visitors 23000
OK
127.0.0.1:6379> HGETALL runoobkey
1) "name"
2) "redis tutorial"
3) "description"
4) "redis basic commands for caching"
5) "likes"
6) "20"
7) "visitors"
8) "23000"
2.3 List:按插入顺序
-
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
-
一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
redis 127.0.0.1:6379> LPUSH runoobkey redis (integer) 1 redis 127.0.0.1:6379> LPUSH runoobkey mongodb (integer) 2 redis 127.0.0.1:6379> LPUSH runoobkey mysql (integer) 3 redis 127.0.0.1:6379> LRANGE runoobkey 0 10 1) "mysql" 2) "mongodb" 3) "redis
2.4 Set:无序、可以取交集、并集
-
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
-
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
-
集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
redis 127.0.0.1:6379> SADD runoobkey redis
(integer) 1
redis 127.0.0.1:6379> SADD runoobkey mongodb
(integer) 1
redis 127.0.0.1:6379> SADD runoobkey mysql
(integer) 1
redis 127.0.0.1:6379> SADD runoobkey mysql
(integer) 0
redis 127.0.0.1:6379> SMEMBERS runoobkey
1) "mysql"
2) "mongodb"
3) "redis"
2.5 有序集合(sorted set) 分数越大顺序越靠后
- Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 - 有序集合的成员是唯一的,但分数(score)却可以重复,分数重复按照插入顺序先后排先后顺序。
- 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
2.6 HyperLogLog和Geo、Stream
-
HyperLogLog:方便计数
-
基数:
-
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
-
-
Geo:存储地理位置(geography)。
-
Stream:Redis Stream 主要用于消息队列(MQ,Message Queue), Redis Stream 提供了消息的持久化和主备复制功能。
2.7 底层数据原理:待补充
3、KEY应用 实际场景
启动redis:
$ redis-cli -h host -p port -a password
设置过期时间:
EXPIRE key seconds
命令:https://www.runoob.com/redis/redis-keys.html
3.1 从海量数据查询具有特定前缀的Key
思路:1、确定数据量(边界)dbsize命令:返回数据量大小
2、数据量小 可以用Keys指令: 语法:Keys [pattern] 单个对象get key(如果为空返回 nil)
3、数据量大 可以用 :SCAN cursor [MATCH pattern] [COUNT count]
问题:Keys:会返回符合条件的所有数据、CPU,I/O压力大、耗时,卡顿。影响线上业务。
SCAN:根据下标和分页查询,比较快。但是数据可能有重复,返回值不一定对应count
返回K1开头的所有Redis: KEYS命令
返回K1开头的所有Redis:SCAN (count 10 可能返回0-10个)
JAVA实现:
定义一个循环,不断调用SCAN指令。直到下标+分页大于 总数。分页取出来的信息,用集合SET去重。
3.2 缓存雪崩、穿透、击穿
3.2.1缓存雪崩:
原理:缓存雪崩:大量缓存数据同时间失效,同事清空大量的缓存,导致用户直接发起大量请求到数据库,产生瓶颈。
解决:
1、生成随机失效的缓存时间数据;
2、让缓存节点分布在不同的物理节点上;
3、生成不失效的缓存数据;
4、定时任务更新缓存数据;
3.2.2 缓存穿透:(穿透攻击)
原理:
用户请求数据,例如ID为负数,不存在缓存里,也不存在数据库里(但是会把空数据放入缓存),会造成缓存穿透。
解决:
1、无意义数据放入缓存,下一次相同请求就会命中缓存;如果参数变化,可以封IP。
2、IP过滤;
3、参数校验(参数-1或者不存在的参数);
4、布隆过滤器;
3.2.3 缓存击穿:(单个KEY失效)
缓存失效:由于缓存热点键(秒杀、热门券)到了失效时间,导致用户请求直接访问数据库。
解决:
1、方案:过期时间(永不过期)。
2、上锁。(zookeeper、redis分布式锁)缓存失效,只有单个线程抢到了锁,单个线程的I/O压力比较小,其余的线程抢不到锁就sleep()。
4、实现分布式锁
4.1 核心问题:
4.1.1互斥性:
原子性操作:SETNX KEY VALUE(返回1 设置成功,返回0设置失败),默认永久有效。可以设置过期时间。
缺陷:不具备原子性:int res=(调用SETNX KEY VALUE)和设置过期时间是两个操作。
如果set过程出现异常,挂掉,就无法设置过期时间。其他线程无法执行CPU独占资源空间。
int res=(调用SETNX KEY VALUE)
if(res=1){//设置成功
设置过期时间
执行CPU独占资源空间
}else{//已经有值
get key获取值
根据业务逻辑判断是否重设过期时间
}
安全性:
死锁:
容错:
4.2用以下原子性命令:
5、异步队列
5.1 List队列:
Rpush生产消息,LPOP消费消息(lpop有值,证明有消息,取不到证明没有值)。
缺点:没有等待队列里有值就去直接消费。
弥补:应用层引入sleep机制去调用LPOP重试。
5.2 BLPOP 单个消息队列
BLPOP key [key ...] timeout:阻塞知道队列有消息或者超时
缺点:只能供一个消费者消费
5.3 PUB/SUB 主题订阅模型
- 发送者(PUBLISH)发送消息,订阅者(SUBSCRIBE )接收消息
- Redis 客户端可以订阅任意数量的频道。
缺点:消息是无状态的,无法保证可达。(订阅者收没收到发送者不知道)
解决:专业MQ kafka等消息对列解决。
6、持久化
6.1 定义:
- 就是把缓存存放到磁盘里面。
- RDB(Redis DataBase)和AOF良种方式
6.2 RDB:Redis DataBase快照持久化
-
是redis默认持久化机制。redis.conf文件里面有redis默认的持久化策略信息。
-
快照持久化:保存(SAVE命令)某个时间点的全量数据快照(Snapshot快照)。如果中间数据发生写入,只会保存写入以前的数据。
-
SAVE:通过主线程来保存缓存到dump.rdb 文件。如果数据量大,比较耗时,阻塞Redis服务器进程。
-
BGSAVE:主线程fork一个子线程来保存缓存到dump.rdb 文件,不阻塞主服务器进程。
缺点:
-
定时快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。
-
内存全量同步,数据量过大会影响性能。
6.2.1具体实现:
手动方式:
- 手动命令保存
自动化方式:
- 可以通过JAVA计时器或者定时任务来调用BGSAVE命令,文件名(fileName+时间),持久化数据库。
6.2.2 BGSAVE 原理
- 简单的实现方式:效率低
- COW:写实复制
6.2.3 恢复rdb文件
- 只要将dump.rdb文件放到我们redis的启动目录即可,redis在启动的时候会自动检查dump.rdb恢复其中的数据。
- 使用
config get dir
命令可以查看启动目录,只要dump.rdb文件在这个目录下,启动就会自动恢复其中的数据。
6.3 AOF:(Append Only File)
6.3.1 原理
-
它是以日志的形式来记录每个写操作,将Redis执行过的所有指令都记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换而言之,Redis重启之后会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
-
AOF方式默认就是文件的无限追加,这样会导致文件会越来越大。
-
AOF默认是关闭状态。
-
AOF和RDB都有的话,优先用AOF。(先判断AOF开没开,没开就用RDB,开了就用AOF)
6.4 RDB、AOF区别:
- Redis 4.0:RDB-AOF混合持久化方式。
- BGSAVE做镜像全量持久化(开机先恢复RDB),AOF做增量持久化(RDB以后的增量数据)。
7、Pipeline:管道
- Pipeline批量执行指令,节省多次I/O往返时间
7.2 Redis的同步机制:主(Master)从(Slave)同步
-
缺点:主服务器挂了就挂了
-
全量同步
- 增量同步
7.3 Redis Sentinel:哨兵机制
8、Redis集群原理
8.1 Redis架构
- redis高并发:主从架构,一主两从三哨兵。一般来说,很多项目其实就足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS。
8.2 Redis复制原理 replication
Redis复制步骤
- (1)slave启动后,给master发送sync指令(同步)
- (2)master接到命令后启动后台的存盘进程,收集所有的缓存数据,收集完成后将整个数据文件同步到slave完成一次同步。
- (3)全量复制:slave服务接收数据文件后,将其存盘加载到内存中。第一次是全量
- (4)增量复制:master继续将新的命令依次传给slave同步。后面是增量
- (5) 只要重连master一次,自动同步将会自动执行
Redis的主从架构复制原理:
- (1)redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量。
- (2)一个master node是可以配置多个slave node的。
- (3)slave node也可以连接其他的slave node。
- (4)slave node做复制的时候,是不会block master node的正常工作的。
- (5)slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了。
- (6)slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量。
8.3 master持久化对于主从架构的安全保障的意义
1、 如果采用了主从架构,那么建议必须开启master node的持久化!
- 不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了。
- master -> RDB和AOF都关闭了 -> 全部在内存中。
- master宕机,重启,是没有本地数据可以恢复的,然后就会直接认为自己IDE数据是空的。
- master就会将空的数据集同步到slave上去,所有slave的数据全部清空
100%的数据丢失。 - master节点,必须要使用持久化机制
2、第二个,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的。
- node可以自动接管master node,但是也可能sentinal还没有检测到master failure,master node就自动重启了,还是可能导致上面的所有slave node数据清空故障。
8.4 Redis复制的基本原理
9、常见问题
9.1 redis和数据库数据同步 Cache Aside Pattern
采用延时双删策略
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
转化为中文描述就是
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存 这么做,可以将1秒内所造成的缓存脏数据,再次删除。
1、Cache Aside Pattern
(1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
(2)更新的时候,先删除缓存,然后再更新数据库
-
一、对强一致要求比较高的,应采用实时同步方案,即先查询缓存查询不到再从DB查询,保存到缓存;更新缓存时,先删除缓存,后更新数据库,再将缓存的设置过期(建议不要去更新缓存内容,直接设置缓存过期)。
-
二、对于并发程度较高的,可采用异步队列的方式同步,可采用kafka等消息中间件处理消息生产和消费。
原因:
方案1:
方案2:
先更新数据库,再更新缓存,并发情况下,线程A
方案3:
9.2 redis主从复制如何保证数据一致性(不丢失)
9.3 Redis主从同步的协议
9.4 Redis过期删除策略和内存淘汰机制
如果假设你设置一个一批key只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
答案是:定期删除+惰性删除
-
所谓定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。假设redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了。注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上redis是每隔100ms随机抽取一些key来检查和删除的。
-
但是问题是,定期删除可能会导致很多过期key到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
-
并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下
-
通过上述两种手段结合起来,保证过期的key一定会被干掉。
-
很简单,就是说,你的过期key,靠定期删除没有被删除掉,还停留在内存里,占用着你的内存呢,除非你的系统去查一下那个key,才会被redis给删除掉。
-
但是实际上这还是有问题的,如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了,咋整?
-
答案是:走内存淘汰机制。
(2)内存淘汰
如果redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:
redis 10个key,现在已经满了,redis需要删除掉5个key
1个key,最近1分钟被查询了100次
1个key,最近10分钟被查询了50次
1个key,最近1个小时倍查询了1次
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
9.5 Redis过期键的删除策略
如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢??如果不是,那过期后到底什么时候被删除呢??
其实有三种不同的删除策略:
(1):立即删除。在设置键的过期时间时,创建一个回调事件,当过期时间达到时,由时间处理器自动执行键的删除操作。
(2):惰性删除。键过期了就过期了,不管。每次从dict字典中按key取值时,先检查此key是否已经过期,如果过期了就删除它,并返回nil,如果没过期,就返回键值。
(3):定时删除。每隔一段时间,对expires字典进行检查,删除里面的过期键。
可以看到,第二种为被动删除,第一种和第三种为主动删除,且第一种实时性更高。下面对这三种删除策略进行具体分析。
立即删除
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力。
而且目前redis事件处理器对时间事件的处理方式–无序链表,查找一个key的时间复杂度为O(n),所以并不适合用来处理大量的时间事件。
惰性删除
惰性删除是指,某个键值过期后,此键值不会马上被删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。所以惰性删除的缺点很明显:浪费内存。dict字典和expires字典都要保存这个键值的信息。
举个例子,对于一些按时间点来更新的数据,比如log日志,过期后在很长的一段时间内可能都得不到访问,这样在这段时间内就要拜拜浪费这么多内存来存log。这对于性能非常依赖于内存大小的redis来说,是比较致命的。
定时删除
从上面分析来看,立即删除会短时间内占用大量cpu,惰性删除会在一段时间内浪费内存,所以定时删除是一个折中的办法。
定时删除是:每隔一段时间执行一次删除操作,并通过限制删除操作执行的时长和频率,来减少删除操作对cpu的影响。另一方面定时删除也有效的减少了因惰性删除带来的内存浪费。
六:redis使用的策略
redis使用的过期键值删除策略是:惰性删除加上定期删除,两者配合使用。