Redis深度历险,全面解析Redis14个核心知识点
一,概述
Redis是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持五种数据类型:字符串,列表,集合,散列表,有序集合。
Redis支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
二,数据类型
Redis数据结构是什么样的
串
>设置问候世界
OK
>获取问候
“世界”
> del hello
(整数)1
>获取问候
(无)
清单
> rpush列表键项目
(整数)1
> rpush列表键项目
2(整数)2
> rpush列表键项目
(整数)3
> lrange列表键0 -1
1)“ item”
2)“ item2”
3) “ item”
> lindex列表键1
“ item2”
> lpop列表键
“ item”
> lrange列表键0 -1
1)“ item2”
2)“ item”
组
> sadd设置键项目
(整数)1
> sadd设置键项目2
(整数)1
> sadd设置键item3
(整数)1
> sadd设置键项目
(整数)0
> smembers设置键
项目1)“ item”
2) “ITEM2”
3) “项目3”
> sismember设定键ITEM4
(整数)0
> sismember设置键项
(整数)1
>斯雷姆组**ITEM2
(整数)1
>斯雷姆组**ITEM2
(整数)0
>成员设置键
1)“ item”
2)“ item3”
杂凑
> hset哈希**子**1值1
(整数)1
> hset哈希**子**2值2
(整数)1
> hset哈希**子**1 值1
(整数)0
> hgetall哈希**
1)“ sub-key1 “
2)” value1“
3)” sub-key2“
4)” value2“
> hdel哈希**子**2
(整数)1
> hdel哈希**子**2
(整数)0
> hget哈希**子**1
“ value1”
> hgetall哈希键
1)“ sub-key1”
2)“ value1”
ZSET
> zadd zset-key 728 member1
(整数)1
> zadd zset-key 982 member0
(整数)1
> zadd zset-key 982 member0
(整数)0
> zrange zset-key 0 -1带有分数
1)“ member1”
2)“ 728“
3)” member0“
4)” 982“
> zrangebyscore zset-key 0 800 withscores
1)” member1“
2)” 728“
> zrem zset-key member1
(整数)1
> zrem zset-key member1
(整数)0
> zrange zset-key 0 -1带分数
1)“ member0 ”
2)“ 982”
三,数据结构
字典
dictht是一个散列表结构,使用拉链法解决哈希冲突。
/ *这是我们的哈希表结构。
对于从旧到新的表,我们实现增量式哈希处理时,每个字典都有两个。* /
typedef struct dictht {
dictEntry ** table;
无符号长号;
无符号长尺寸掩码;
长期使用无符号;
} dictht;
typedef struct dictEntry {
void * key;
union {
void * val;
uint64_t u64;
int64_t s64;
双d;
} v;
struct dictEntry *下一步;
} dictEntry;
Redis的字典dict中包含两个哈希表dictht,这是为了方便进行rehash操作。在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上,完成之后释放空间并交换两个dictht的角色。
typedef struct dict {
dictType * type;
无效* privdata;
htht [2];
长期的rehashidx / *如果rehashidx == -1 * /
未签名的长迭代器,则不会进行哈希处理;/ *当前运行的迭代器数量* /
} dict;
rehash操作不是一次性完成,或者采用渐进方式,这是为了避免一次性执行过多的rehash操作给服务器带来过大的负担。
渐进式rehash通过记录dict的rehashidx完成,它从0开始,然后每执行一次rehash都会转换。例如在一次rehash中,要把dict [0] rehash到dict [1],这一次会把dict [0]上表[rehashidx]的键值对rehash到dict [1]上,dict [0]的表[rehashidx]指向null,并令rehashidx ++。
在rehash期间,每对字典执行添加,删除,查找或更新操作时,都会执行一次渐进式rehash。
采用渐进式rehash会导致字典中的数据分散在两个dictht上,因此对字典的查找操作也需要到对应的dictht去执行。
/ *执行N步增量重哈希。如果仍有
*键从旧的哈希表移至新的哈希表,则返回1,否则返回0。
*
*请注意,重新哈希处理步骤包括将一个存储桶(在
使用链接时,存储桶中的* 可能超过一个键)从旧的哈希表移至新的哈希表,但是
*由于哈希表的一部分可能由空白组成,
* 不能保证此函数即使在单个存储桶中也能重新哈希,因为它
*总共最多可以访问N * 10个空存储桶,否则
它的工作量将不受限制,并且该函数可能会长时间阻塞。* /
int dictRehash(dict * d,int n){
int empty_visits = n * 10; / *要访问的最大空桶数。* /
如果(!dictIsRehashing(d))返回0;
while(n-- && d-> ht [0] .used!= 0){
dictEntry * de,* nextde;
/ *注意rehashidx不会溢出,因为我们确定还有更多
*元素,因为ht [0] .used!= 0 * /
assert(d-> ht [0] .size>(unsigned long)d-> rehashidx );
而(d-> ht [0] .table [d-> rehashidx] == NULL){
d-> rehashidx ++;
如果(--empty_visits == 0)返回1;
}
de = d-> ht [0] .table [d-> rehashidx];
/ *将此存储桶中的所有键从旧哈希HT * /
while(de){
uint64_t h;
nextde = de-> next;
/ *获取新哈希表中的索引* /
h = dictHashKey(d,de-> key)&d-> ht [1] .sizemask;
de->
d-> ht [1] .table [h] = de;
d-> ht [0] .used--;
d-> ht [1] .used ++;
de = nextde;
}
d-> ht [0] .table [d-> rehashidx] = NULL;
d-> rehashidx ++;
}
/ *检查我们是否已经重新整理了整个表格... * /
if(d-> ht [0] .used == 0){
zfree(d-> ht [0] .table);
d-> ht [0] = d-> ht [1];
_dictReset(&d-> ht [1]);
d-> rehashidx = -1;
返回0;
}
/ *要重新哈希的内容... * /
返回1;
}
跳跃表
是有序集合的实力实现之一。
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找22的过程。
与红黑树等平衡树评分,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;更容易实现;支持无锁操作。
四,使用场景
计数器
可以对String进行自增自减运算,从而实现计数器功能。
Redis这种内存类型数据库的读写性能非常高,很适合存储重复读写的计数量。
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
发现表
例如DNS记录就很适合使用Redis进行存储。
发现表和缓存类似,也是利用了Redis快速的查找特性。但是查找表的内容不能重新设置,而缓存的内容可以重新设置,因为缓存不作为可靠的数据来源。
消息数值
列表是一个双向链表,可以通过lpush和rpop写入和读取消息
不过最好使用Kafka,RabbitMQ等消息中间件。
会话缓存
可以使用Redis来统一存储多台应用服务器的会话信息。
当应用服务器不再存储用户的会话信息时,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个串口上的进程进行同步。
可以使用Redis自带的SETNX命令实现分布式锁,另外,还可以使用官方提供的RedLock分布式锁实现。
其他
设置可以实现交集,并集等操作,从而实现共同好友等功能。
ZSet可以实现有序性操作,从而实现排行榜等功能。
五,Redis与Memcached
两者都是非关系型内存键值数据库,主要有以下不同:
数据类型
Memcached仅支持字符串类型,而Redis支持五种不同的数据类型,可以更灵活地解决问题。
数据持久化
Redis支持两种持久化策略,而Memcached不支持持久化。
分散
Memcached不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的中断。
Redis Cluster实现了分布式的支持。
内存管理机制
- 在Redis中,并不是所有数据都一直在存储中,可以将一些很久没用的值交换到磁盘,而Memcached的数据可以一直在内存中。Memcached将内存分割成特定长度的块来存储数据。 ,,以完全解决内存碎片的问题。但是这种方式方式会占用内存的利用率不高,例如块的大小为128字节,仅存储100字节的数据,那么剩下的28字节就浪费掉了。
六,键的过期时间
Redis可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的替代元素设置过期时间。
七,数据淘汰策略
可以设置内存最大使用量,当内存使用量超过时,会施行数据淘汰策略。
Redis具体有6种淘汰策略:
作为内存数据库,出于对性能和内存消耗的考虑,Redis的消除算法实际实现上并非针对所有**,而是替换一个小部分和从中选出被淘汰的**。
使用Redis缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为焦点数据占用的内存量,然后启用allkeys-lru淘汰策略,将最近一次减少使用的数据淘汰。
Redis 4.0删除了volatile-lfu和allkeys-lfu淘汰策略,LFU策略通过统计访问频率,将访问频率替换为最小的键值对淘汰。
八,持久化
Redis是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
RDB持久化
将某个时间点的所有数据都存放到硬盘上。
可以将快照复制到其他服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,保存快照的时间会很长。
AOF持久化
将写命令添加到AOF文件(仅附加文件)的末尾。
使用是AOF持久化需要设置同步选项,从而确保将命令写入同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到堆栈,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
- always选项会严重减少低服务器的性能; everysec选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且Redis每秒执行一次同步对服务器性能几乎没有任何影响;没有选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
Redis提供了一种将AOF重写的特性,能够删除AOF文件中的多余写命令。
九,事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其他客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提高性能。
Redis最简单的事务实现方式是使用MULTI和EXEC命令将事务操作包围起来。
十,事件
Redis服务器是一个事件驱动程序。
文件事件
服务器通过专有与客户端或其他服务器进行通信,文件事件就是对众多操作的抽象。
Redis基于Reactor模式开发了自己的网络事件处理器,使用I / O多路交替程序来同时监听多个交替,传递到达的事件传送给文件事件分派器,分派器会根据众多产生的事件类型调用相应的事件处理器。
时间事件
服务器有一些操作需要在给定的时间点执行,时间事件是对此类定时操作的抽象。
时间事件又分为:
- 定时事件:是让一段程序在指定的时间之内执行一次;发生事件:是让一段程序每隔指定时间就执行一次。
Redis将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。
事件的调度与执行
服务器需要不断监听文件事件的充分的能力得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件调度与执行由aeProcessEvents函数负责,伪代码如下:
def aeProcessEvents():
#获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
#计算最接近的时间事件距离到达还有很多时间
remaind_ms =time_event。when-unix_ts_now()
#如果事件已到达,那么,remaind_ms的值可能为负数,将其设置为0,
如果remaind_ms <0:
remaind_ms = 0
#根据remaind_ms的值,创建timeval
timeval = create_timeval_with_ms(remaind_ms)
#并发等待文件事件产生,最大时间由情况的timeval决定
aeApiPoll(timeval)
#处理所有已产生的文件事件
procesFileEvents()
#处理所有已到达的时间事件
processTimeEvents()
将aeProcessEvents函数放置到一个循环里面,加上初始化和清理函数,就构成了Redis服务器的主函数,伪代码如下:
def main():
#
初始化服务器init_server()
#一直处理事件,直到服务器关闭为止,
而server_is_not_shutdown():
aeProcessEvents()
#服务器关闭,执行清理操作
clean_server()
从事件处理的角度来看,服务器运行流程如下:
十一,复制
通过使用主机端口的slave命令来让一个服务器成为另一个服务器的从服务器。
一个从服务器只能有一个主服务器,并且不支持主主复制。
连接过程
- 主服务器写入快照文件,发送给从服务器,并在发送期间使用错误记录执行其写命令。快照文件发送完毕之后,开始向从服务器发送存储在串口中的写命令;从服务器提供所有旧数据,,加载主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;主服务器每执行一次写命令,就向从服务器发送相同的写命令。
主从链
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
十二,前哨
Sentinel(哨兵)可以监听植入中的服务器,并在主服务器进入下线状态时,自动从服务器中选举出新的主服务器。
十三,分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以线性等级的性能提升。
假设有4个Redis实例R0,R1,R2,R3,还有很多表示用户的键user:1,user:2,... ,,有不同的方式来选择一个指定的键存储在其中实例中。
- 最简单的方式是范围分片,例如用户id从0〜1000的存储到实例R0中,用户id从1001〜2000的存储到实例R1中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用CRC32哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的中断上。服务器分片:Redis簇。
十四,一个简单的论坛系统分析
该论坛系统功能如下:
- 可以发布文章;可以对文章进行点赞;在首页可以按文章的发布时间或者文章的点赞数进行排序显示。
文章信息
文章包括标题,作者,赞数等信息,在关系型数据库中很容易堆积一张表来存储这些信息,在Redis中可以使用HASH来存储单个信息以及其对应的值的映射。
Redis没有关系型数据库中的表这一概念来将同种类型的数据放置在一起,或者使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储ID ,通常使用:来进行分隔。例如下面的HASH的键称为article:92617,其中article为命名空间,ID为92617。
点赞功能
当有用户为一篇文章点赞时,除了要对该文章的投票进行转换进行1操作,还必须记录该用户已经该文章进行了点赞,防止用户点赞次数超过1。可以建立文章的已投票用户集合来进行记录。
为了节约内存,规定文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个星期的过期时间可以实现这个规定。
对文章进行排序
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。的有序集合分值并非直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
学习分享,共勉
资料免费领取方式:加群:739289671 即可获取文档资料的免费领取方式和这份JVM学习脑图(内含很多笔记)!!
Java架构进阶资料展示