敖丙思维导图-Redis

敖丙思维导图系列目录

这些知识整理都是自己查阅帅丙资料(本文还参考了《Redis深度历险:核心原理和应用实践》这本书)加以总结滴~ 每周都会更新知识进去。
如有不全或错误还请大家在评论中指出~


  1. 敖丙思维导图-集合
  2. 敖丙思维导图-多线程之synchronized\ThreadLocal\Lock\Volatitle\线程池
  3. 敖丙思维导图-JVM知识整理
  4. 敖丙思维导图-Spring
  5. 敖丙思维导图-Redis
  6. 敖丙思维导图-RocketMQ+Zookeeper
  7. 敖丙思维导图-Mysql数据库
  8. 敖丙思维导图-网络基础
  9. 敖丙思维导图-Dubbo

本文章目录


在这里插入图片描述

Redis快速的原因

  • 纯内存操作。
  • 单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题。

Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。
使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。6.0版本带来了多线程特性,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗,。-》多线程任务可以分摊 Redis 同步 IO 读写负荷。
Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。

  • 采用了非阻塞I/O多路复用机制。多路指的是多个socket连接,复用指的是复用一个线程。Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll的read、write、close等都转换成事件,不在网络I/O上浪费过多的时间。

当有多个请求发送到服务端的时候,实际上会有一个文件事件处理器同时监听多个套接字,并且根据套接字目前执行的任务来关联不同的事件处理器。
事件处理器只需要将它们做绑定即可,io多路复用程序是会将所有产生的套接字都存入一个有序且同步的队列中,最后redis会有逐一地对这个队列中的元素进行处理。
epoll没有最大并发连接的限制,只管你“活跃”的连接 ,而跟连接总数无关。Epoll使用了“共享内存 ”,省去内存拷贝。

非阻塞I/O

每个tcp socket创建时,os会为它分配读缓冲区、写缓冲区。
非阻塞I/O在套接字上提供了一个选项 Non_Blockiing,这个选项打开后,读写方法不会阻塞。能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区空闲空间字节数

  1. 阻塞式IO (处理一个socket就要占用一个线程)
    让出CPU,进到等待队列,等socket就绪后再次获得时间片继续执行。
  2. 非阻塞式IO
    不让出CPU,频繁检查socket就绪状态(忙等待,难把握轮询间隔,空耗CPU)
  3. IO多路复用 (一次系统调用,监听多个socket)
    操作系统提供支持,把需要等待的socket加入到监听集合。

多路复用(事件轮询)

非阻塞I/O有个问题,线程要读数据,结果读了一部分就返回了,那么如何知道何时该继续呢?事件轮询解决。

事件轮询的API,就是Java中的NIO技术

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。
UNIX操作系统提供了select/poll/epoll这样的系统调用。你告知我一批套接字(socket),当这些套接字的可读或可写事件发生时,我通知你这些事件信息。多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

程序注册一组socket文件描述符给操作系统,表示"我要监视这些fd是否有IO事件发生,有了就告诉程序处理"。

以前都是用select但是

  1. 16*64=1024 最多监听1024个fd
  2. 每次调用select都要传入所有监听集合,频繁的从用户态到内核态拷贝数据。
  3. 每次都要遍历所有集合,判断哪个fd是可操作的
    epoll解决了这些问题

Redis数据结构底层实现

1. String (动态字符串sds(Simple Dynamic String, 简单动态字符串)代替c字符串)

String 缓存结构体用户信息,计数( value 是一个整数,还可以对它使用 INCR 命令进行 原子性 的自增操作)。
Redis 为了对内存做极致的优化,采用预分配庸余空间减少内存的频繁分配
SDS 与 C 字符串的区别:

  • 获取字符串长度为 O(N) 级别的操作 → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组;
  • 不能很好的杜绝 缓冲区溢出/内存泄漏 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题;
  • C 字符串 只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码
struct sdshdr {
   
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 字节数组
    char buf[];
};

没有直接采用c语言自带的字符串,好处有以下几点:
减少原先繁琐的内存扩增问题。(会根据初始化的值,提前给出更多的空间,避免出现空间溢出问题)
通过空间预分配机制来减少内存重分配问题。

2. 字典Hash (“数组 + 链表” 的链地址法来解决部分 哈希冲突)

保存结构体信息可部分获取不用序列化所有字段。
实际上字典结构的内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 渐进式 rehash

3. 双向链表 List (相当于LinkedList链表,栈和队列都能实现)

twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现(支持反向查找和遍历)

  • LPUSH 和 RPUSH 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素;
  • LRANGE 命令可以从 list 中取出一定范围的元素;
  • LPOP 命令可以从移出并获取列表的第一个元素
异步消息队列

使用rpush/lpush操作入队列,使用 blpop 和 brpop(阻塞读) 来出队列。

如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般
会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常来。

4. 集合 Set (相当于HashSet,集合的元素具有唯一性,无序性)

去重的场景,交集(sinter)、并集(sunion)、差集(sdiff),实现如共同关注、共同喜好。
它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。

sadd key member 添加一个 string 元素到 key 对应 set 集合中,成功返回 1,如果元素以及 在集合中则返回 0

5. 有序列表Zset ( SortedSet + HashMap,每个唯一 value 赋予一 score 值,用来代表排序的权重 )

可用来实现延时队列、排行榜。内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

当zsort的score相同的情况下,redis是以key的字典序进行排名的。
一级以外的维度不变的情况下可以直接用 key 排序,比较简单。
如果维度会更新,可以使用拆分二进制或十进制的方法存储,二进制的优点是存储的数比较大,而且可以用位运算。
十进制的优点是计算简单,可读性比较好。各个维度的长度还可以做成配置项,这样就可以满足不同的业务需求了。

跳跃列表

最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」
在这里插入图片描述

跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。首先 L0 层肯定是 100% 了,L1 层只有 50% 的概率,L2 层只有 25% 的概率,L3 层只有 12.5% 的概率,一直随机到最顶层 L31 层。绝大多数元素都过不了几层。(很公平)

整数集合(只包含整数值元素,并且这个集合的元素数量不多)

当一个集合(Set)只包含整数值元素,并且这个集合的元素数量不多时, Redis i就会使用整数集合作为集合键的底层实现。

整数集合升级过程

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,3需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。

整数集合升级的优点

  • 提升灵活性 (因为C语言是静态类型语言,为了避免类型错误,我们通常不会将两种不同类型的值放在同一个数据结构里面。)

  • 节约内存

  • 整数集合是Redis自己设计的一种存储结构,集合键的底层实现之一。

  • 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。

  • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存。

  • 整数集合只支持升级操作,不支持降级操作

Zset分数相同的时候自定义排序规则

Redis 默认实现是,相同分数的成员按字典顺序排序(0 ~9 , A ~Z,a ~ z),所以相同价格排序就不能根据时间优先来排序。

分数 = 价格 + 时间 (当前系统时间戳)
分数为64 位的长整型 int64_t, 价格作为高位存储, 时间作为低位存储,时间精度上面,精确到秒级别。

 int64_t分数,二进制用高 32位存价格,低32位存储当时与某一个时刻的时间差(秒),那么数据看起是这样
 这里有一个最大时间 MAX_TIME = 2208960000(2040年1月1日)(服务超过这个时间无效)
 A 玩家,(10 * 价格偏移) + MAX_TIME - 11111111111111( 时间戳)
 B 玩家,(10 * 等级偏移) + MAX_TIME - 1111122222( 时间戳)
 最终分数A > B ,
 最终排序,A 玩家会排到B前面。通过分数可以解析出真实价格和时间

距离当前时间的毫秒值之差作为小数部分,得分(整数)作为整数部分,存入缓存;从缓存取出得分时

  • 18
    点赞
  • 151
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值