目录
Redis是一个key-value存储的内存缓存操作,其数据库完全在内存中,使用磁盘仅用于持久性。Redis可以将数据复制到任意数量的从服务器。
Redis的优点:
1、速度快,性能好
2、支持丰富的数据类型,支持String,List,set,sorted set,hash
3、支持事务,操作都是原子性的,保证了如果两个客户端同时访问的Redis服务器将获得更新后的值
4、Redis是一个多实用的工具,可用于缓存,消息,按key设置过期时间,过期后将会自动删除
Redis的缺点:
1、由于是内存数据库,所以单台机器存储的数据量跟机器本身的内存大小有关。虽然redis本身有key过期策略,但是还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
2、如果进行完整重同步,由于需要生成rdb文件,并进行传输,会占用主机的CPU,并会消耗现网的带宽。
3、修改配置文件,进行重启,将硬盘中的数据加载进内存,时间比较久。在这个过程中,redis不能提供服务。
Redis的适用场景
- 缓存
由于Redis访问速度块、支持的数据类型比较丰富,所以Redis很适合用来存储热点数据。另外Redis提供了键过期功能,也提供了灵活的键淘汰策略,可以设置过期时间然后再进行缓存更新操作。 - 排行榜
借助Redis的SortedSet进行热点数据的排序。很多网站都有排行榜应用的,如月度销量榜单、商品按时间的上新排行榜等。 - 计数器
Redis提供的原子性的自增操作incrby命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。INCR key:将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 - 分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 - 分布式锁
实现思想:使用setnx加锁,如果返回1,则说明加锁成功,并设置超时时间,避免系统挂了,锁没法释放。在finally中delete删除锁释放。如果需要设置超时等待时间,则可以加个while循环,在获取不到锁的情况下,进行循环获取锁,超时了则退出。 - 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口。 又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。 - 最新动态
按照时间顺序排列的最新动态,也是一个很好的应用,可以使用 Sorted Set 类型的分数权重存储 Unix 时间戳进行排序。 - 消息队列
Redis 能作为一个很好的消息队列来使用,除了Redis自身的发布/订阅模式,依赖 List 类型利用 LPUSH 命令将数据添加到链表头部,通过 BRPOP 命令将元素从链表尾部取出。 - 发布订阅
通过哨兵模式互相监督的,因为每隔2秒哨兵节点会发布对某节点的判断和自身的信息到某频道,每个哨兵订阅该频道获取其他哨兵节点和主从节点的信息,以达到哨兵间互相监控和对主从节点的监控。
发布端和订阅者通过channel关联。channel的订阅关系维护在Redis实例级别,独立于redisDB的key-value体系。所有的channel都由一个map维护,键是channel的名字,value是它所有订阅者client的指针链表。
Redis数据结构
Redis支持五种数据类型:String、Hash、List,Set、及ZSet(sorted set:有序集合)。
- Redis 的String 可以包含任何数据,如数字,字符串,jpg图片或者序列化的对象。使用场景:
缓存功能:字符串最经典的使用场景,Redis作为缓存层,MySQL作为储存层,绝大部分请求数据都是Redis中获取,由于Redis具有支撑高并发特性,所以缓存通常能起到加速读写和降低 后端压力的作用
计数器:可以实现快速计数、查询缓存的功能
共享session:出于负载均衡的考虑,分布式服务会将用户信息的访问均衡到不同服务器上,用户刷新一次访问可能会需要重新登录,为避免这个问题可以用Redis将用户session集中管理,在这种模式下只要保证Redis的高可用和扩展性的,每次获取用户更新或查询登录信息都直接从Redis中集中获取 - Redis Hash 是一个键值(key->value)对集合;是一个 String 类型的 field 和 value 的映射表。使用场景:哈希结构相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷,常常用于用户信息等管理
- List 就是链表(Redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。使用场景:消息队列,Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端是用lupsh从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞时的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性
- Redis的Set是String类型的无序集合。和链表一样,在执行插入和删除和判断是否存在某元素时。集合最大的优势在于可以进行交集并集差集操作。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。使用场景:标签(tag),如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴 趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签
- Redis ZSet和Set一样也是String类型元素的集合且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。ZSet的成员是唯一的,但分数(score)却可以重复。ZSet是插入有序的,即自动排序。使用场景:排行榜。例如视频网站需要对用户上传的视频做排行榜,可以按照时间、按照播放量、按照获得的赞数等进行榜单维护。
ZSet底层存储结构
ZSet底层的存储结构包括ziplist或skiplist,在同时满足以下两个条件的时候使用ziplist,其他时候使用skiplist,两个条件如下:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素的长度小于64字节
当ziplist作为ZSet的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

当skiplist作为ZSet的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。
zskiplist作为skiplist的数据结构,包括指向头尾的header和tail指针,其中level保存的是skiplist的最大的层数。zskiplistNode的数据格式,每个节点有保存数据的robj指针,分值score字段,后退指针backward便于回溯,zskiplistLevel的数组保存跳跃列表每层的指针。
zset存储过程
zset的添加过程以zadd的操作进行分析,整个过程如下:
- 解析参数得到每个元素及其对应的分值
- 查找key对应的zset是否存在,不存在则创建
- 如果存储格式是ziplist,那么在执行添加的过程中需要区分元素存在和不存在两种情况,存在情况下先删除后添加;不存在情况下则添加并且需要考虑元素的长度是否超出限制或实际已有的元素个数是否超过最大限制进而决定是否转为skiplist对象
- 如果存储格式是skiplist,那么在执行添加的过程中需要区分元素存在和不存在两种情况,存在的情况下先删除后添加,不存在情况下那么就直接添加,在skiplist当中添加完同时需要更新dict的对象
Redis的五种数据结构的内部编码

1.字符串的内部编码
字符串类型的内部编码有3种:
int:8个字节的长整型。
embstr:小于等于39个字节的字符串。
raw:大于39个字节的字符串。
Redis会根据当前值的类型和长度决定使用内部编码实现。
2.哈希的内部编码
哈希类型的内部编码有两种:
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现。因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
3.列表的内部编码
列表类型的内部编码有两种:
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)。同时所有值都小于hash-max-ziplist-value配置(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现。
linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
4.集合的内部编码
集合类型的内部编码有两种:
intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合内部实现,从而减少内存的使用。
hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
5.有序集合的内部编码
有序集合类型的内部编码有两种
ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个)。同时每个元素的值小于zset-max-ziplist-value配置(默认64个字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存使用。
skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时zip的读写效率会下降。
Redis的bitmap结构
在开发中,可能会遇到这种情况:需要统计用户的某些信息,如果使用普通的 key/value存储,用户量很大,需要的空间也会很大,所以 Redis提供了Bitmap位图,Bitmap是通过操作二进制位来进行记录,节约内存。
优点:
节省空间: 通过一个bit位来表示某个元素对应的值或者状态,其中key就是对应元素的值。实际上8个bit可以组成一个Byte,所以是极其节省空间的。
效率高: setbit和getbit的时间复杂度都是O(1),其他位运算效率也高。
Bitmap实现原理
Java 是如何表示用 Bitmap 来表示上述的大数组的?首先定义一个 byte 数组,根据初始容量来分配数组的大小 capacity = ( size >> 3 ) + 1:
public byte[] bitemapArray(int size){
byte[] arr = new byte[(size >> 3) + 1];
return arr;
}
byte数组的容量为什么是(size>>3)+1?还是因为1个字节=8bit
比如传入的大小size=7,则capacity=(7>>3)+1=1
之后,向数组中添加值,即把对应的位设置为 1,步骤如下:
- 首先找到 byte 中的索引 index,计算公式 index = n / 8 = n >> 3
- 找到 byte[index] 中对应的位置 position ,计算公式 position = n % 8 = n & 0x07 (因为下标从 0 开始)
- 将 byte[inex] 和 1 << positon 做或(|)运算
Redis过期策略与淘汰机制
Redis过期策略是:定期删除+惰性删除
定期删除指Redis默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。但是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,所以有了惰性删除,即当获取某个 key 的时候,Redis首先检查取出的键是否过期,若过期删除该键,否则,返回该键。
但是如果定期删除漏掉了很多过期 key,然后也没及时去查,也就没走惰性删除,如果大量过期 key 堆积在内存里,导致 Redis内存块耗尽了,这时候进行内存淘汰机制。
内存淘汰机制
Redis中的默认的过期策略是noeviction 。
maxmemory-policy 六种方式:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
Redis客户端与服务器的交互模式
1、串行的请求/响应模式
每一次请求的发送都依赖于上一次请求的相应结果完全接收,同一个连接的每秒吞吐量低。Redis对单个请求的处理时间通常比局域网的延迟小一个数量级,所以串行模式下,单链接的大部分时间都处于网络等待。
2、双工的请求/相应模式(pipeline)
适用于批量的独立写入操作,即可将请求数据批量发送到服务器,再批量地从服务器连接的字节流中一次读取每个响应数据,减少了网络延迟,所以单连接吞吐量较串行会提高一个数量级。
3、原子化的批量请求/响应模式(事务)
客户端通过和Redis服务器两阶段的交互做到批量命令原子执行的事务效果:入队操作(即服务器端先将客户端发送过来的连接对象暂存在请求队列中)和执行阶段(依次执行请求队列中的所有请求)。一个连接的请求在执行批量请求的过程中,不会执行其他客户端的请求。Redis的事务不是一致的,没有回滚机制。如果中途失败,则返回错误信息,但已经成功执行的命令不会回滚。
事务里面有可能会带有读操作作为条件,由于批量请求只会先入队列,再批量一起执行,所以一般读操作不会跟批量写请求一起执行,这时候就有可能会导致批量写之前和之后读到的数据不一致,这种可以通过乐观锁的可串行化来解决,Redis通过watch机制实现乐观锁,流程为:
1、将本次事务涉及的所有key注册为观察模式
2、执行只读操作
3、根据只读操作的结果组装写操作命令并发送到服务器端入队
4、发送原子化的批量执行命令EXEC试图执行连接的请求队列中的命令
5、如果前面注册为观察模式的key中有一个或多个,在EXEC之前被修改过,则EXEC将直接失败,拒绝执行;否则顺序执行请求队列中的所有请求
6、Redis没有原生的悲观锁或者快照实现,但可通过乐观锁实现,一旦两次读到的操作不一样,watch机制触发,拒绝了后续的EXEC执行
Redis处理命令的主要逻辑
Redis服务器对命令的处理都是单线程的,但是I/O层面却面向多个客户端并发地提供服务,并发到内部单线程的转化通过多路复用框架来实现
1、首先从多路服用框架(epoll、evport、kqueue)中select出已经ready的文件描述符(fileDescriptor),ready的标准是已有数据到达内核(kernel)、已准备好写入数据
2、对于上一步已经ready的fd,redis会分别对每个fd上已ready的事件进行处理,处理完相同fd上的所有事件后,再处理下一个ready的fd。有3中事件类型:
acceptTcpHandler:连接请求事件
readQueryFromClient:客户端的请求命令事件
sendReplyToClient:将暂存的执行结果写回客户端
3、对来自客户端的命令执行结束后,接下来处理定时任务(TimeEvent)
4、aeApiPoll的等待时间取决于定时任务处理(TimeEvent)逻辑
5、本次主循环完毕,进入下一次主循环的beforeSleep逻辑,后者负责处理数据过期、增量持久化的文件写入等任务
Redis命令的原子性如何保证?
Redis本身提供的所有API都是原子操作:一个操作的不可以再分,操作要么执行,要么不执行。Redis是单线程的事件循环,一个操作执行完了才执行下一个操作。
Redis通信协议
Redis客户端和服务端之间通信的协议是RESP(Redis的序列化协议),传输层使用TCP。RESP实际上是一个支持以下数据类型的序列化协议:字符串(Strings),错误(Errors),整数(Integers),批量字符串(Bulk String)和数组(Arrays)。
本文全面介绍了Redis,它是内存缓存操作,速度快、支持丰富数据类型和事务。阐述了其适用场景,如缓存、排行榜等;详细讲解了数据结构,包括ZSet底层存储等;还介绍了bitmap结构、过期策略、客户端与服务器交互模式、命令处理逻辑、原子性保证及通信协议。
21万+

被折叠的 条评论
为什么被折叠?



