Redis也算是使用很久了,但是知识点一直很零碎,没有系统的整理过,这次整理虽然有自己的一些总结,不过绝大多数还是从其他博客收集整理而来,仅限学习交流使用。
1、redis安装步骤
#下载、解压、编译:
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
tar xzf redis-5.0.5.tar.gz
cd redis-5.0.5
make
#将Redis启动路径添加至环境变量中,方便对Redis进行操作:
vim /etc/profile
在/etc/profile添加:
export PATH=$PATH:/usr/local/redis/redis-5.0.5/src
source /etc/profile
2、redis定义
redis是使用C语言开发的一个开源的(遵循BSD规范)高性能键值对(Key-Value)的内存数据库,可以作为数据库、缓存、消息中间件等
是一种NoSql(not-only sql 泛指非关系型数据库)数据库
3、redis特点
- 作为一个内存数据库,性能优秀,数据在内存中,读写速度非常快,支持并发10W QPS
- 单进程单线程,是线程安全的
- 采用IO多路复用机制
- 丰富的数据类型,支持字符串(String)、哈希(hash)、列表(list)、集合(set)、有序集合(sorted set)
- 支持数据持久化,可以将内存中的数据保存到磁盘中,重启时加载
- 主从复制、哨兵模式、高可用
- 可以用作分布式锁(RedLock)
- 可以作为消息中间件使用,支持发布订阅
4、redis支持的5种数据类型解析
String字符串类型
String类型是二进制安全的,意思是Redis的String类型可以包含任何数据,比如JPG图片或者序列化的对象,String类型的值最大能存储512M
Hash哈希
Hash是一个键值(key-value)的集合。Redis 的 Hash 是一个 String 的 Key 和 Value 的映射表,Hash 特别适合存储对象。常用命令:hget,hset,hgetall 等
List列表
List 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等
应用场景:List 应用场景非常多,也是 Redis 最重要的数据结构之一,比如 Twitter 的关注列表,粉丝列表都可以用 List 结构来实现
数据结构:List 就是链表,可以用来当消息队列用。Redis 提供了 List 的 Push 和 Pop 操作,还提供了操作某一段的 API,可以直接查询或者删除某一段的元素
实现方式:Redis List 的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销
Set集合
Set 是 String 类型的无序集合。集合是通过 hashtable 实现的。Set 中的元素是没有顺序的,而且是没有重复的。常用命令:sdd、spop、smembers、sunion 等
应用场景:Redis Set 对外提供的功能和 List 一样是一个列表,特殊之处在于 Set 是自动去重的,而且 Set 提供了判断某个成员是否在一个 Set 集合中
Sorted Set有序集合
Sorted Set 和 Set 一样是 String 类型元素的集合,且不允许重复的元素。常用命令:zadd、zrange、zrem、zcard 等
使用场景:Sorted Set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序
当你需要一个有序的并且不重复的集合列表,那么可以选择 Sorted Set 结构
和 Set 相比,Sorted Set关联了一个 Double 类型权重的参数 Score,使得集合中的元素能够按照 Score 进行有序排列,Redis 正是通过分数来为集合中的成员进行从小到大的排序
实现方式:Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射
而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 Score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单
5、Redis使用实际可能遇到的问题
数据库和缓存数据一致性问题
一致性问题是分布式常见的问题,还可以再分为强一致性和最终一致性,数据库和缓存双写,就必然会存在不一致问题,如果对数据有强一致性要求,就不能放缓存,我们所做的一切,只能保证最终一致性。另外我们所做的方案从根本上来说,只能降低不一致发生的概率,无法完全避免。
数据库和缓存双写一致性问题解决方案:
首先,采取正确的更新策略,先更新数据库,再删除缓存,延时双删策略
其次,因为可能存在缓存删除失败的问题,所以需要再提供一个补偿措施,例如消息队列或者订阅数据binlog
缓存雪崩问题
缓存雪崩,即缓存在同一时间大面积失效,请求全部转发到DB,DB瞬时压力过大导致连接异常或者崩溃。
缓存雪崩问题解决方案:
- 给缓存的失效时间加一个随机值,避免集体失效
- 使用互斥锁,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据
- 永不过期,这里的永不过期分为两类,第一是物理永不过期,即不设置失效时间;第二是逻辑永不过期,即设立守护线程,当缓存快过期时自动续期;
- 双缓存策略,使用本地缓存(ehcache)
缓存穿透问题
缓存穿透是指查询一个一定不存在的数据,由于缓存是查询不命中是被动写的,并且出于容错考虑,如果从存储层查询不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,这就失去了缓存的意义。当并发流量很大时,DB可能会挂掉,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞
缓存穿透问题解决方案:
- 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
- 缓存空数据,如果一个查询返回的数据为空,我们仍然把这个结果进行缓存,但是缓存过期时间会很短,最长不要超过五分钟
缓存击穿问题
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
缓存击穿问题解决方案:
- 设置缓存永不过期,物理永不过期或者逻辑永不过期
- 使用互斥锁(分布式锁)
缓存并发竞争问题
缓存并发竞争问题主要体现在并发写竞争
缓存并发竞争问题解决方案:
(1)分布式锁+数据修改时间戳
- 想要向缓存中写入数据时,必须获得分布式锁,只有获得了分布式锁才可以去进行缓存数据的写入,写入结束释放锁,可以保证同一时刻只有一个客户端写缓存。
- 分布式锁并不能保证每个客户端获取锁的顺序,但是我们要写入缓存的数据都是从数据库查询出来的,数据库都是有这种数据的创建时间的,所以可以在更新之前, 先去对比自己的这条数据的时间和缓存中数据的时间,谁更新,如果自己更新则写入覆盖,否则直接放弃本次操作
(2)乐观锁(时间戳或版本号)
6、redis过期策略
定期删除+惰性删除
定期删除:redis默认每个100 ms检查,是否有过期的key,如果有过期key则删除。
需要说明的是,redis不是每个100 ms将所有的key检查一次,而是随机抽取进行检查(如果每个100 ms,全部key进行检查,redis会卡死)。
因此如果只采用定期删除策略,会导致很多key到期没有被删除。
惰性删除:当你获取某个key的时候,redis会检查一下,这个key是否设置了过期时间,如果设置了过期时间那么这个key是否过期了,如果过期了就会删除。
定期删除+惰性删除并不能完全清除过期的缓存数据,长时间运行的话,redis的内存会越来越高,那么就应该采用缓存淘汰机制
7、缓存淘汰机制
在redis.conf中有一行配置
maxmemory-policy volatile-lru
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
8、redis为什么这么快
纯内存操作
单线程操作,避免了频繁的上下文切换
非阻塞多路IO复用机制
9、redis持久化
(1)RDB快照
在默认情况下,Redis将内存数据库快照保存到dump.rdb的二进制文件中。
可以对Redis进行设置,可以让他“在N秒内数据集至少有M个改动”,这一条件被满足时,自动保存一次数据集。
比如说让Redis满足“在60秒内数据集至少有1000个改动”,这一条件被满足时,自动保存一次数据集
save 60 1000
除了在配置文件中使用save设置RDB快照,还可以在命令行中手动执行命令生成RDB快照,进入Redis客户端执行命令save或bgsave可以生成dump.rdb文件
每次执行命令都会将所有redis内存快照保存到一个rdb文件里,并覆盖原有的rdb内存快照文件
save是同步命令,bgsave是异步命令,bgsave会从redis主进程fork出一个子进程专门用来生成rdb二进制文件
(2)AOF(append only file)
RDB快照并不能完全保证持久化,如果redis因为某些原因而造成故障停机,那么服务器将丢失最近写入而未保存到内存快照中的那些数据。
从1.1版本开始,Redis新增了一种可以完全持久化(durable)的方式:AOF持久化,将修改的每一条指令记录进appendonly.aof文件中
修改配置文件来打开aof功能
appendonly yes
打开aof功能,每当redis执行一个改变数据集的命令时,这个命令就会追加到aof文件的末尾,这样的话,当redis重新启动时,程序就会通过执行aof文件中的命令来达到重建数据集的目的。
可以配置redis多久才将命令持久化到磁盘一次
appendfsync always 每次有新命令追加到aof文件时,都持久化一次,非常慢但安全
appendfsync everysec 每秒执行一次持久化,足够快(和使用rdb持久化差不多)并且在Redis故障时只会丢失1秒的数据
appendfsync no 从不持久化,将数据交给操作系统来处理,redis处理命令速度加快但是不安全
默认情况下,采用每秒执行一次持久化,这种持久化策略可以兼顾安全和速度
redis启动时如果既有rdb文件又有aof文件,则优先选择aof文件恢复数据,因为aof文件一般来说数据更安全一点。
AOF重写:
aof文件里可能会有太多“琐碎”的指令,会拖慢数据恢复速度,所以aof会定期根据内存最新的数据重新生成aof文件。
有两个配置可以控制aof自动重写的频率
auto-aof-rewrite-min-size 64mb aof文件至少要达到64m才会触发制动重写,文件太小恢复速度本来就很快,重写的意义不大
auto-aof-rewrite-percentage 100 aof文件上一次重写后文件大小增长了100%则再次触发重写
当然aof还可以手动重写,进入redis客户端执行命令bgrewritaof重新aof文件
触发aof重写时,redis会fork一个子进程去做,不会对redis正常处理命令有太多影响。
(3)Redis 4.0 混合持久化
重启redis恢复数据集时,很少会使用rdb来恢复内存状态,因为会丢失大量数据,通常会使用aof日志恢复数据,但是重放aof日志性能相对rdb来说要慢的多,这样在redis实例很大的情况下,启动需要花费很长时间。
Redis 4.0为了解决这个问题,带来了新的持久化选项:混合持久化
aof-use-rdb-preamble yes
如果开启了混合持久化,aof在重写时,不再是单纯将内存数据转换为RESP命令写入aof文件,而是将重写这一刻之前的内存做rdb快照处理,并且将rdb快照内容和增量的aof修改内存数据的命令存放在一起,都写入新的aof文件,新的aof文件一开始不叫作appendonly.aof,等到重写完成之后,新的aof文件才会进行改名,原子的覆盖原有的aof文件,完成新旧两个aof文件的替换。
于是在redis重启的时候,可以先加载rdb快照文件,然后再重放增量的aof日志,就可以完全替代之前的aof全量文件重放,因此重启效率得到大幅提高。
10、Redis跳跃表(skipList)
- 跳跃表基于单链表加索引的方式实现
- 跳跃表以空间换时间的方式提升了查找速度
- Redis有序集合在节点元素数量较大或者元素数据量较多时使用跳跃表实现
- Redis的跳跃表实现由zskiplist和zskiplistnode两个结构组成,其中zskiplist用于保存跳跃表信息(例如表头节点、表尾节点、长度等),而zskiplistnode则用于表示跳跃表节点
- Redis每个跳跃表节点的层高都是1-32之间的随机数
- 同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的,跳跃表中的节点按分值大小进行排序,分值相同时,节点按成员对象的大小进行排序
跳跃表有如下性质:
- 由很多层结构组成
- 每一层都是一个有序链表
- 最底层(Level1)的链表包含所有元素
- 如果一个元素出现在Level n的链表中,则它在Leven n以下的链表中都会出现
- 每个节点包含两个指针,一个指向同一个链表中的下个节点,一个指向下面一层的元素
学习跳跃表
对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)
如果我们想要提高其查找效率,可以考虑在链表上建索引的方式。每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引
这个时候,我们假设要查找节点8,我们可以先在索引层遍历,当遍历到索引层中值为 7 的结点时,发现下一个节点是9,那么要查找的节点8肯定就在这两个节点之间。我们下降到链表层继续遍历就找到了8这个节点。原先我们在单链表中找到8这个节点要遍历8个节点,而现在有了一级索引后只需要遍历五个节点。
从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍的结点个数减少了,也就是说查找效率提高了,同理再加一级索引
从图中我们可以看出,查找效率又有提升。在例子中我们的数据很少,当有大量的数据时,我们可以增加多级索引,其查找效率可以得到明显提升
像这种单链表加多级索引的结构就是跳跃表
跳跃表的问题思考
为什么有序集合包含的元素数量比较多或者成员是比较长的字符串的时候Redis要使用跳跃表来实现?
从上面我们可以知道,跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。
原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。
11、Redis主从复制
和mysql主从复制原因一样,Redis虽然数据读取写入的速度都非常快,但是也会产生读压力特别大的情况,为了分担读压力,Redis支持主从复制。
Redis的主从结构可以采用一主多从或者级联结构。
Redis主从复制可以分为全量同步和增量同步
(1)全量同步
Redis全量复制一般发生在Slave(从节点)初始化阶段,这时Slave需要将Master上的所有数据都复制一份,具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令
- 主服务器接收到SYNC命令后,开始执行BGSAVE命令生成RDB快照文件并使用缓冲区记录此后执行的所有写命令
- 主服务器BGSAVE命令执行完成后,向所有从服务器发送RDB快照文件,并在发送期间继续记录被执行的写命令
- 从服务器收到RDB快照文件后丢弃所有旧数据,并载入收到的RDB快照
- 主服务器快照发送完毕后,开始向从服务器发送缓冲区中的写命令
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令
完成了上面几个步骤后就完成了从服务器数据初始化的所有操作,此时从服务器就可以接收来自用户的读请求了
(2)增量同步
Redis增量同步是指Slave初始化后开始正常工作时,主服务器发生的写操作同步到从服务器的过程
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令
(3)Redis主从同步策略
- 主从刚刚连接的时候,进行全量同步
- 全量同步结束后,进行增量同步
- 如有需要,Slave可以在任何时候发起全量同步
- 先尝试增量同步,如不成功,要求Slave进行全量同步
(4)注意事项
如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机
(5)主从复制的一些特点
- 采用异步复制;
- 一个主redis可以含有多个从redis;
- 每个从redis可以接收来自其他从redis服务器的连接;
- 主从复制对于主redis服务器来说是非阻塞的,这意味着当从服务器在进行主从复制同步过程中,主redis仍然可以处理外界的访问请求;
- 主从复制对于从redis服务器来说也是非阻塞的,这意味着,即使从redis在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从redis返回的是以前老的数据,如果你不想这样,那么在启动redis时,可以在配置文件中进行设置,那么从redis在复制同步过程中来自外界的查询请求都会返回错误给客户端;(虽然说主从复制过程中,对于从redis是非阻塞的,但是当从redis从主redis同步过来最新的数据后还需要将新数据加载到内存中,在加载到内存的过程中是阻塞的,在这段时间内的请求将会被阻, 但是即使对于大数据集,加载到内存的时间也是比较多的);
- 主从复制提高了redis服务的扩展性,避免单个redis服务器的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案;
- 为了减小主redis服务器写磁盘压力带来的开销,可以配置让主redis不再将数据持久化到磁盘,而是通过连接让一个配置的从redis服务器及时的将相关数据持久化到磁盘,不过这样会存在一个问题,就是主redis服务器一旦重启,因为主redis服务器数据为空,这时候通过主从同步可能导致从redis服务器上的数据也被清空;
12、RedLock分布式锁
高效分布式锁基础
- 安全属性:互斥,不管任何时候,只有一个客户端能持有同一个锁
- 效率属性A:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕机或者发生网络分区
- 效率属性B:容错,只要大多数Redis节点正常工作,客户端都应该能获取锁和释放锁
RedLock原理
- 获取当前时间(单位毫秒)
- 轮流用相同的Key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时会有一个和总的锁释放时间小得多的超时时间,比如锁自动释放时间为10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过多时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
- 客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁,而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
- 如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
- 如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1),还是因为获取锁消耗的时间超过了锁释放时间,客户端都回到每个master节点上释放锁,即便是那些它认为没有获取成功的锁。