Redis基础知识
Redis基础知识汇总
使用Redis有哪些好处?
- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 支持丰富数据类型,支持string,list,set,sorted set,hash
- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
相关知识:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略:
voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
redis和memcache的区别
- 数据类型:mem只支持字符串,redis支持5种不同的数据类型
- 数据持久化:redis支持两种持久化策略:RDB快照和AOF日志,而mem不支持持持久化。
- 分布式:mem不支持分布式,只能通过客户端使用一致性解哈希实现分布式存储,这种方式在存储和查询时都需要现在客户端计算一次数据所在的节点。
- redis Cluster 实现了分布式的支持。
- 内存管理机制:在redis中,并不是所有的数据都一直存储在内存中,可以将一些很久没用的value交换到磁盘,而mem的数据则一直都会在内存中。
- mem将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为128bytes,只存储100bytes的数据,那么剩下的29bytes就浪费掉了。
- memcache处理请求时使用多线程异步IO的方式,可以合理利用CPU的多核优势,性能非常优秀。
- me功能简单,使用内存存储数据
redis的特点
redis采用单线程模式处理请求:
- 采用了非阻塞的异步事件处理机制
- 缓存数据都是内存操作IO时间不会太长,单线程可以避免线程上下文切换产生的代价。
redis支持持久化,所以redis不止可以用作缓存,也可以用作Nosql数据库
redis除去kv之外支持多种格式:string、list,set、sorted set、hash
redis提供主从同步机制,以及Cluster集群部署能力,能够提供高可用服务
基础数据类型
- string:最常用的数据类型
方法: set get
缓存功能,利用string作为缓存配合其他数据库大大增加系统的读写速度以及降低后端数据库的压力。
计数器,使用redis作为计数器,快速实现计数和查询的功能(点赞),而且可以永久保存。
共享用户Session ,用户刷新页面可能需要访问一次数据库进行登录状态的识别,或者访问页面的缓存Cookie,但是
可以利用Redis将用户的Session集中管理,只要保证redis高可用,每次用户Session的更新和获取都可以快速完成,提 高效率。分布式锁的实现: 分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
数据结构
字符串的数据结构采用了SDS结构,简单动态字符串

- 该结构中定义了属性len,可以在0(1)的时间获得字符串的长度
- 内存的预分配:字节数组用于存储真正的字符串,该字节数组中会有空闲的位置,空闲的数量由字段free来记录,因此增长字符串的时候不会经常需要进行重新的内存分配,而C语言中的字符串增长或者减少都会进行内存的分配。
- 二进制安全,在C中必须符合某种编码,在SDS中会以二进制的方式处理字节数组中的值,可以存储照片,音频等二进制数据。
List:列表
**场景:**通过list存储一些列表类型的数据结构,类似于粉丝列表、文章评论列表 **高性能分页:** 比如可以通过Irange命令,读取某个闭区间内的元素,可以基于List实现分页查询。列表不但有序,还支 持范围内获取元素,类似于微博下拉不断分页。 **消息队列:list是一个双向链表,可以通过lpush和rpop写入和读取消息(不过最好使用Rabbitmq和kafka)**
双端:链表有prev和next指针,所以是一个双端链表
无环:表头节点的prev指针和表尾的next都指向了null
多态:链表节点使用void *指针来保存节点值。、
1 |
|
- Set:集合,可以实现去重功能
**基于Set将需要去重的数据扔进去,系统部署在多台机器,实现全局自动去重。使用Set进行交集,差集,并集。
交集:共同好友、差集:推荐好友、并集:
利用唯一性做统计:统计访问网站的所有独立IP;点赞、收藏、抽奖等。
整数集合可以实现自动升级,即在原来c语言中的存储数字会产生一处,而在结合中存储不会存在内存溢出的情况。**
1 |
|
ZSet:有序集合,去重并且排序,写入时有一个分值,根据分值自动排序
条件:当你选择有序并且不重复的集合列表时,就可以选择sorted Set数据结构
场景1:排行榜、热搜榜, 维护榜单可能是多方面的:按照时间、按照播放量、按照获得的点赞数。
场景2:做带权重的队列,普通消息为1,重要消息为2,工作线程就可以选择按照score的倒序来获取工作任务。
1 |
|
- Hash:类似于map的一种结构
场景:将结构化的数据比如(一个对象)前提是这个对象里没有嵌套其他对象,缓存到redis里,然后每次读写缓存的时候,可以就操作Hash里的某个字段。
1 |
|
redis字典redis中字典根据hash实现的dictht,table是一个数组,数组中的每个元素都是字典结构的指针,这个字典结构保存着一个键值对,多个键值对是通过链表来实现。字典这个节骨存在next指针
1 |
|
dictEntry结构,next 属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一次,
以此来解决键冲突(collision)的问题。
1 |
|
dict,type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:
type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis
会为用途不同的字典设置不同的类型特定函数。
而 privdata 属性则保存了需要传给那些类型特定函数的可选参数。
ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对
ht[0] 哈希表进行 rehash 时使用。
除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash ,
那么它的值为 -1 。
1 |
|
dictType
1 |
|
有序集合是在集合的基础上提供了一个成员分数作为排序;
发表时间作为分数,就可以得到按照时间先后顺序的记录;
将分数当做权重,重要的消息权重大,优先处理;
分数存储好友的亲密度
有序集合使用跳跃表作为底层实现:
跳表由很多层组成
跳表的每一层都是一个有序链表
跳表除了第一层,每个节点都包含了两个指针,一个指向相同层的下一个节点,一个指向上一层的相同节点。
最底层的元素包含了所有的元素;
redis底层跳表解析:http://redisbook.com/preview/skiplist/datastruct.html
跳跃表的基础依然是链表,它是在链表的基础上,定义了很多层,可以看做多个有序链表;最底层包含了所有的数据,每增加一层存储的数据为每个节点进行连接,数据为相邻下一层的一半。随着层数的增加,每一层的数据量也在逐步减少,redis中有序集合采用跳表的结构可以快速的进行二分查找。
跳跃表
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。

在查找时,从上层指针开始查找,找到对应区间之后,再到下一层寻找,下图演示了查找到22的过程。

与红黑树等平衡树相比,跳表具有以下优点:插入删除增加都是O(logn)
插入速度非常快速,因为不需要旋转等操作来维持平衡性
更容易实现
支持无锁操作
键的过期时间:
redis可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期的时间。
数据淘汰策略:
可以设置内存最大使用量,当内存使用量超出时,施行数据淘汰策略。
redis有6种淘汰策略
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止驱逐数据 |
redis的淘汰算法实际实现上并非针对所有的key,而是抽样一小部分并从中选出被淘汰的key
使用redis缓存数据的时候,为了提高命中率,需要保证缓存数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启动allkeys-lru淘汰策略,将最近最少使用过的数据淘汰。
redis4.0引入了volatile-lfu和allkeys-lfu淘汰策略,LFU策略通过统计访问频率,将访问频率最少的键值对淘汰。
持久化
redis是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久保存到硬盘上。
RDB持久化:将某个时间点的所有数据都存放到硬盘上
可以将快照复制到其他服务器从而创建具有相同数据的服务器副本
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,快照保存的时间会很长
AOF持久化:将写命令添加到AOF文件(Append only File)的末尾
使用AOF持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件写入并不会马上同步到磁盘,而是先存储在缓存区,然后由操作系统决定什么时候同步到磁盘。
随着服务器写请求的增多,AOF文件会越来越大,redis提供了一种将AOF重写的特性,能够去除AOF文件中的冗余写命令。
选项 | 同步频率 |
---|---|
always | 每个写命令都同步(会严重降低性能) |
everysec | 每秒同步一次(崩溃只会丢失1秒的数据) |
no | 让操作系统来决定何时同步(不会提升性能,增加了丢失的数据的数量) |
事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其他客户端命令的请求。
事务中的多个命令被一次性发送个服务器,而不是一条一条发送,这种方式被称之为流水线,它可以减少客户端与服务器之间的网络通信次数从而提高性能。
redis最简单的事务实现方式是使用MULTI和EXEC命令将事务操作包围起来。
事件
redis服务器是一个事件驱动程序。
文件事件:服务器通过套接字与客户端或者其他服务器进行通信,文件事件就是对套接字操作的抽象。
高可用分布式集群
高可用
高可用(High Availability),是当一台服务器停止服务后,对于业务及用户毫无影响。
停止服务的原因可能由于网卡、路由器、机房、CPU负载过高、内存溢出、自然灾害等不可预期的原因导致,在很多时候也称单点问题。
解决单点问题的方案: 主备方式 和主从复制
主从复制:
一主多从,主从之间进行数据同步,当Master宕机之后通选举算法(Raft),从slave中选举出新的Master继续对外提供服务
读写分离:主机进行写,从机进行读,分担压力.
主节点挂了之后,不会在slave中选取新节点
缺点:
主机宕机之后,slave虽然被选中为master,但是对外提供服务的IP地址发生了变化,需要通知客户端,客户端收到新的地址之后,使用新地址发送请求.(
这应该是哨兵模式的缺点了)
主备方式:
一台主机,多台备机,在正常情况下,主机对外服务,并把数据同步到备机,当主机宕机之后,备机立即开始服务,RedisHa使用比较多的就是keeplived,它使得主备机对外提供同一个虚拟IP,客户端通过虚拟IP进行数据操作,正常期间主机一直对外提供服务,宕机后IP自动跳到备机.
数据同步
异步方式:主机收到写操作之后,直接返回成功,然后再后台用异步的方式把数据同步到从机上,这种同步性能比较好,但无法保证数据的完整性,比如在异步同步过程中主机宕机
同步方式:当主机收到写操作之后,以同步的方式把数据同步到从机,性能会降低.
哨兵模式:
建立在主从复制基础之上
当master挂了之后使用选举算法(Raft)选一个slave作为新的master
sentinel因为也是一个进程有挂掉的可能性,所以哨兵也会启动多个形成一个哨兵集群
多个哨兵配置的时候,哨兵之间会自动监控
当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中
一个哨兵或者哨兵集群可以管理多个主从Redis,多个哨兵也可以监控同一个redis
哨兵最好不要和redis部署到同一台服务器,不然redis挂了,哨兵也挂了
哨兵的工作机制:
每个哨兵会以每秒一次的频率向他所知的master,salve以及其他的哨兵实例发送一个ping命令
如果一个实例距离最后一次有效回复Ping的命令的时间超过了down-after-millseconds选项所指定的值,则会被标记为主观下线
如果一个master被标记为主观下线,则正在监视这个master的所有哨兵都要以每秒一次的频率确定master的确已经进入主观下线的状态
当有足够数量的哨兵的时候,在指定的时间范围内确认了master的确进入了主观下线状态,则master会被标记为客观下线
在一般情况下,每个哨兵都会以每10秒一次的频率向它已知的所有master,slave发送INFO命令
当master被哨兵标记为客观下线的时候,哨兵会向下线的master的所有的slave发送INFO的命令从10秒1次改成课1秒一次
redis集群Cluster模式
多台搭载redis的服务器,提供相同的服务,从而让服务器达到一个分布式,高可用的redis服务.
哨兵模式基本可以满足一般的生产需求,具备高可用性,但是当服务器数据量大到一台服务器放不下的情况时,主从和sentinel模式就不能满足需求了,这个时候就需要对存储的数据进行分片,将数据存储到多个Redis实例中,Cluster模式的出现就是为了解决单机Redi容量有限的问题,将Redis的数据根据一定的规则分配到多台机器.
cluster可以说是哨兵和主从模式的结合体,通过cluster就可以实现主从master选举功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例,因为Redis的数据是根据一定的规则分配到cluster的不同机器的,当数据量过大的时候,就可以新增机器进行扩容.
redis集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现,一个redis集群中会有16384个哈希槽,使用公式计算键应该存放在哪个槽
CRC16(key)%16384
2.Redis分布式锁
和Memcached的方式类似,利用Redis的setnx命令。此命令同样是原子性操作,只有在key不存在的情况下,才能set成功。(setnx命令并不完善,后续会介绍替代方案)
3.Zookeeper分布式锁
利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。
首先讲一下Redis的分布式锁,这种实现方式比较有代表性。
如何用Redis实现分布式锁?
Redis分布式锁的基本流程并不难理解,但要想写得尽善尽美,也并不是那么容易。在这里,我们需要先了解分布式锁实现的三个核心要素:
1.加锁
最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给key命名为
“lock_sale_商品ID” 。而value设置成什么呢?锁的value值为一个随机生成的UUID。我们可以姑且设置成1。加锁的伪代码如下:
1 |
|
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
2.解锁
有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令,伪代码如下:
1 |
|
释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
3.锁超时
锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。
所以,setnx的key必须设置一个超时时间,单位为second,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放,避免死锁。setnx不支持超时参数,所以需要额外的指令,伪代码如下:
1 |
|
综合起来,我们分布式锁实现的第一版伪代码如下:
1 |
|
上面的伪代码只是分布式锁的简单实现,结合实际应用场景考虑就会发现上述分布式锁的实现存在着三个致命问题:
redis缓存穿透、缓存击穿、雪崩
缓存击穿
是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
缓存击穿的解决方案: 单体应用采用互斥锁,就是当缓存击穿之后,给一个请求上互斥锁,然后请求完之后,把数据放入redis,其他用户请求redis
即可。
缓存穿透,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
例子:比如我有一个网站,侵犯了某些人的利益,则他们就利用缓存穿透去攻击我的网站,然后redis缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。然后就通过脚本不断的让我的数据库返回查询失败,把我的机器搞趴下。
缓存穿透解决方案
- IP过滤;
- 缓存空对象,设置过期时间
- 参数校验;
- 布隆过滤器;
(1)布隆过滤器
布隆过滤器是一种数据结构,就是一个二进制数组,判断一个数据存不存在这个数组中,存在就是1,不存在就是0.
比如你好,经过三个哈希函数,第一个hash把你好算成3 第二个hash把你好算成5,第三个hash把你好算成7,会把下标为3,5,7的数据变为1,这样你好就存到了布隆过滤器中去了。
二进制数据必须都是1才能证明这个数据存在,比如hello也是在1这个位置,所以不知道这个位置到底是哪个数据,把0变为1是删除了,所以很难删除。
优点: 二进制组成非常快
插入和查询非常快,计算数据的hash值,再由hash值映射到数据的下标,基于数组的特性他的插入和查询是非常快的,只需要根据响应的数据就可以了。时间复杂度
o(k),有k个hash函数就是O(k)y因为每个hash都要去数组中做一个操作。
保密性能非常好
缺点:很难删除
误判:不同的数据计算出来的hash值是相同的,所以会存在误判的情况。
减少误判的概率需要将hash函数变多,这就是减少误判的一个因素。
先把数据放到布隆过滤器,如果请求布隆过滤器没有,就直接返回,如果有才去请求redis,布隆过滤器的空间占用很小布隆过滤器一般使用redis的bitmap来存储。
垃圾网站和正常网站加起来全世界据统计也有几十亿个。网警要过滤这些垃圾网站,总不能到数据库里面一个一个去比较吧,这就可以使用布隆过滤器。假设我们存储一亿个垃圾网站地址。
可以先有一亿个二进制比特,然后网警用八个不同的随机数产生器(F1,F2, …,F8) 产生八个信息指纹(f1, f2, …, f8)。接下来用一个随机数产生器
G 把这八个信息指纹映射到 1 到1亿中的八个自然数 g1, g2, …,g8。最后把这八个位置的二进制全部设置为一。过程如下:

有一天网警查到了一个可疑的网站,想判断一下是否是XX网站,首先将可疑网站通过哈希映射到1亿个比特数组上的8个点。如果8个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。
那这个布隆过滤器是如何解决redis中的缓存穿透呢?很简单首先也是对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。
这个形式很简单。
2、缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存雪崩
概念
缓存雪崩是指,缓存层出现了错误,不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决方案
- 生成随机失效的缓存时间数据;
- 让缓存节点分布在不同的物理节点上;
- 生成不失效的缓存数据;
- 定时任务更新缓存数据;
(1)redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
(2)限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
(3)数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。