Redis学习记录笔记

一.前言
   在聊Redis之前,我们不妨抽象开来,聊一下关系型数据库与非关系型数据库。我一直认为一门新的技术或者说中间件的诞生,必然是有其自身的应用场景。非关系型数据库的应用场景需要和关系型数据库混合起来看。对于其传统的关系型数据库来看,他的特征是关系,表作为业务单据的载体,表与表之间的联系支撑起来整个复杂的业务系统。但是随着业务的持续发展,数据的大量涌入,表与表之间的关系变得复杂而又冗余。当数据大到一定程度的时候,单纯的纵向扩展变得成本巨大,因此横向扩展变成了新兴话题。于是 分库分表,分布式,集群变得了主流,当然这些东西在这里暂时不表。随着业务系统的再一次扩大,我们系统中难免出现这样一种场景,大量的碎片的数据(数据中台的应用也使得nosql应用更加广泛),这些数据间没有强烈的关联关系,而这些数据很适合redis。因为redis最一开始就是字典表。
二.关系型数据库与非关系型数据的差异
关系型数据库的特点:
1、它以表格的形式,基于行存储数据,是一个二维的模式。
2、它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构。
3、表与表之间存在关联(Relationship)。
4、大部分关系型数据库都支持 SQL(结构化查询语言)的操作,支持复杂的关联查询。
5、通过支持事务(ACID 酸)来提供严格或者实时的数据一致性。
但是使用关系型数据库也存在一些限制,比如:
1、要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据的存储,就要扩大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表。
2、表结构修改困难,因此存储的数据格式也受到限制。
3、在高并发和高数据量的情况下,我们的关系型数据库通常会把数据持久化到磁盘,基于磁盘的读写压力比较大。
非关系型数据库的特点:
1、存储非结构化的数据,比如文本、图片、音频、视频。
2、表与表之间没有关联,可扩展性强。
3、保证数据的最终一致性。遵循 BASE(碱)理论。 Basically Available(基本可用); Soft-state(软状态); Eventually Consistent(最终一致性)。
4、支持海量数据的存储和高并发的高效读写。
5、支持分布式,能够对数据进行分片存储,扩缩容简单。
以此来看,关系型数据库与非关系型数据库两者之间对立统一。两者之间本质上都是去做持久化,区别在于存储的格式以及特性的不同。
不过以往的开发过程中,他的实际使用除了海量碎片化的存储外还有很多其他的,比如说缓存,分布式token,分布式锁,消息队列,全局ID(给分库分表用的),点赞,签到,计数,热度排行榜。
而当我反过来看redis的相关应用场景的时候,我发现他其实本质上由三个部分组成。
1.其本身特性:
        redis的创建初衷就是一个KV的库,他的直接目的就在于能够快速读取数据(redis全称叫做 Remote Dictionary Service 远程字典服务),那么当我们需要频繁访问一些热点数据,那么我们就会优先想到缓存他。再加上它能够做持久化(区别于内存数据库的地方),且读写效率很高,所以第一个应用场景缓存就来了。
2.其数据结构特性:
        这些特性由他的数据结构本身决定,因为他的原子递增,他的单线程,他的LRU策略,所以演变出很多应用场景,点赞,计数,排行榜.....
3.在分布式环境中的特性:
        在分布式环境中,所有的微服务之间相互独立,而很多全局性的东西就只能依托于第三方管控,这就是redis的在分布式中的地位,比如说分库分表的全局Id,比如全局Token。(PS:在我的理解中zookeeper与Eureka属于分布式微服务之间的服务发现与注册,而redis是游离在整个分布式系统架构外面,搞全局的。)
三.基础概念以及思维导图
这边先聊一下具体的思维导图和一些学习中发现的亮点
1.跳表:
  官方文档中有跳表的详细描述  跳跃表的实现 — Redis 设计与实现
这里单独把跳表拿出来说,是因为他当时令我眼前一亮。在以往的链表中,我们的时间遍历是O(n),跳表以空间作为牺牲,换取了遍历复杂度的降低。相当于他把每个key设置了对应的高度,在每个区间范围内命中数据。
2.字典表以及Hash相关
首先需要确认的是redis创建的初衷就是一个KV的字典表(虽然他现在的功能远远不止这些)。在redis中Hash表用dictht定义
大概是这样的类图 dict里面包了ht数组,ht数组里面是dictht,dictht里面是具体的entry键数组,entry内部才是真正的dictEntry的值
而这个单独抽出来是为何呢,因为他的rehash是两个dictht,而这个是用来进行rehash的。redis的rehash叫做渐进式rehash,我们先聊rehash
1.进行rehash的原因
       随着操作的不断进行,哈希表保存的键值对会逐渐的增多或减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或太少,就对哈希表进行扩展或收缩。
2.rehash的步骤
  (1)为字典的ht[1]哈希表分配空间
     若是扩展操作,那么ht[1]的大小为>=ht[0].used*2的2^n
      若是收缩操作,那么ht[1]的大小为>=ht[0].used的2^n
(2)将保存在ht[0]中的所有键值对rehash到ht[1]中,rehash指重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
(3)当ht[0]的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],新建空白的哈希表ht[1],以备下次rehash使用。
而这个的做法思路其实就是JVM内的复制删除算法。(就是S1 S2两个区域互相复制,不得不提一句,好的思路往往是相似的)。
而渐进式的说法,是因为整个rehash过程并不是一步完成的,而是分多次、渐进式的完成。如果哈希表中保存着数量巨大的键值对时,若一次进行rehash,很有可能会导致服务器宕机。
  渐进式我理解的就是先给h[1]开辟空间,设置标记位,进行分批同步数据。如果在同步过程中有对h[0]进行操作,则这个数据记录继续同步到h[1]。最终h[1]同步完成,标记位设置-1。而这个很像redis主从同步fork子线程这块。
3.redis的“快”
redis的快的因素有以下几点:
1.存内存操作
2.网路模块单线程
3.异步非阻塞IO(多路复用)
纯内存操作:一般来说内存操作远高于磁盘读写(不考虑固态硬盘顺序读写走索引)。在查看相关文档后,redis是纯内存操作,数据读写在内存中完成,然后RDF,AOF等将数据持久化到物理磁盘上。
这里提出一个命题,为何我们常常说内存操作远高于磁盘操作
在我的理解中,不是说磁盘就慢,内存就快。更多的是他们的读取方式导致的快慢的差异,比如说我存储一个链表他的数据在磁盘中可能就不是连续的了,那我通过物理介质读取这个数据,磁盘就要不停的转。好比我们买硬盘的时候,他就会说这个转速多少,转速代表了读取的速度,而相同大小的数据,如果在磁盘中时分散存储的,他就要转很多圈。但是内存就没有这个问题,其实不管在内存中数据的结构如何,红黑树也好,链表也好,时间复杂度O(n)也好,O(log(n))也罢。他至少没有一个碎片数据不断转的过程。这是我理解的磁盘慢于内存的根本原因。
所以说如果有一天你的磁盘时顺序存储(不需要转很多圈,可能几圈就把数据读取出来了),或者有一天你有一个索引,类似于Inndb的多路平衡树,你也不一定就会比内存慢。在kafak中就有这样一个命题“磁盘顺序读写大于内存随机读写”。当然这个要研究一下。
其实笔者之前纠结了很久,既然是存内存操作,那如果我服务器重启,内存会重新加载磁盘数据吗?如果重新加载磁盘数据的话,我是全量加载还是部分加载,如果磁盘数据远远大于内存空间怎么办?抱着这些疑问,我下载了redis的源码,然后我发现我第一没学过C,第二英文实在不咋地。(我忽然觉得spring源码挺友好的,至少他是java,虽然他跳来跳去)
这篇文章讲了,redis的内存,我觉得挺有价值的。博客链接   理解Redis的内存_令仔很忙-CSDN博客_redis内存
Redis的内存主要包括:对象内存+缓冲内存+自身内存+内存碎片。
1、对象内存
    对象内存是Redis内存中占用最大一块,存储着所有的用户的数据。Redis所有的数据都采用的是key-value型数据类型,每次创建键值对的时候,都要创建两个对象,key对象和value对象。key对象都是字符串,value对象的存储方式,五种数据类型–String,List,Hash,Set,Zset。每种存储方式在使用的时候长度、数据类型不同,则占用的内存就不同。
2、缓冲内存
    主要包括:客户端缓冲、复制积压缓冲区、AOF缓冲区
    客户端缓冲:普通的客户端的连接(大量连接),从客户端(主要是复制的时候,异地跨机房,或者主节点下有多个从节点),订阅客户端(发布订阅功能,生产大于消费就会造成积压)
    复制积压缓冲:2.8版本之后提供的可重用的固定大小缓冲区用于实现部分复制功能,默认1MB,主要是在主从同步时用到。
    AOF缓冲区:持久化用的,会先写入到缓冲区,然后根据响应的策略向磁盘进行同步,消耗的内存取决于写入的命令量和重写时间,通常很小。
3、内存碎片
    目前可选的分配器有jemalloc、glibc、tcmalloc默认jemalloc
    出现高内存碎片问题的情况:大量的更新操作,比如append、setrange;大量的过期键删除,释放的空间无法得到有效利用
    解决办法:数据对齐,安全重启(高可用/主从切换)。
4、自身内存
    主要指AOF/RDB重写时Redis创建的子进程内存的消耗,Linux具有写时复制技术(copy-on-write),父子进程会共享相同的物理内存页,当父进程写请求时会对需要修改的页复制出一份副本来完成写操作。
多路复用:
    Redis服务器是一个事件驱动程序,服务器处理两类事件,一类是文件事件(redis客户端与服务器套字链接操作的抽象),一类是时间事件(定点执行事件)。而在文件事件中,redis是使用多路复用监听多个套字,并通过目前的任务来为套字关联不同的事件处理器
这里redis使用的是将一个socket套字链接分成多段,每段去做请求,这个时候我交给的是第三方的handle_events事件处理机制,此刻对于当前请求就是异步
用户线程轮询I/O操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路I/O复用模型也被称为异步阻塞I/O模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用I/O多路复用模型时,socket都是设置为NONBLOCK的
我认为这里需要掌握两个点,第一套字的产生可能是多个的,所以事件可能会并发的出现,但是所有的套字都会放在一个队列里,然后通过这个队列以有序,同步,每次一个套字的形式发送给文件事件分派器。第二,IO多路复用允许服务器同时监听可读和可写事件,如果一个套字他既有可读又有可写,那么会优先处理读,再去处理写。
4.内存过期/淘汰策略
redis中使用定期过期和惰性过期两种方式。惰性过期就是我在用的时候查看一下这个过期了没有,这样对cpu很友好,对内存不好。定期过期就是类似计划任务触发去刷这些过期的key。这里其实还有个定时过期,类似于每一个key都去维护一个监听器,用于判断Key的是否存活。这种方式对内存很友好。但是这种方式对应CPU消耗很高。
对于淘汰策略,redis有两大类
LRU(Least Recently Used)
根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”
常用的实现方式是将所有的访问对象放在Hash中,队列按照FIFO进行排列,如果之后又访问到此数据,则将这个对象移动到队列底。
Redis通过随机采样来调整算法的精度。他通过选举N多个个Key,然后计算出热度最低的数据,把它们从内存中删除
可以说采样的数据越大,越接近真实值,但是cpu消耗越高
关于如何选举出热度最低的数据,redis在全局变量中维护了一个当前时间的时间搓(定时任务100ms刷一下)
然后每一个key上面都有一个字段用于记录lru的值,当对象被创建和修改时 更新这个值
而这个字段上的lru的值与系统的时间搓相差越大,则代表着数据越不活跃,越容易被淘汰
LFU,Least Frequently Used,最不常用
redis内部用一个24Bits用于维护LFU,他的高16位记录访问时间,低8位记录访问频次
当key被访问的时候 lfu增加,增加的频次和counter(增加因子)有关
当key在n多分钟内(衰减因子)没有被访问时,key衰减
增加LFU的最大用处就是
按照LRU来进行计算 我A被访问了3次
B被访问了20次,但是由于A我是最近访问的,反而容易把我的B给淘汰了
其实Redis还有很多东西要继续和学习,但是有些东西我自己没有深入的玩过,我觉得只是停留在理论知识界面上会比较尴尬。
四.redis的一些实际案例
4.1并发锁
我认为这是redis的最重要的一个使用方式。
这是线程内部使用锁
4.2 限流 滑动窗口
4.3 限流 Lua脚本 通过incr的原子递增
4..4日活统计 位运算
    
4.5 物理距离
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值