源码模块

  • 基础数据结构
    • 字符串结构(sds)
    • 快速查找的跳跃表(skiplist)
    • 节约内存的压缩列表(ziplist)
    • 基于hash表实现的字典(dict)
    • 基于链表(list)和压缩列表(ziplist) ==》得到的快表(quickList)
    • 消息队列Stream(基于listpack以及基数(rax))
  • 数据类型
  • 数据库实现
  • 服务端实现
  • 集群/主从/队列

不同层次源码介绍

  • Redis的基本数据结构
  • Redis的数据类型
  • 持久化实现、主从复制、集群

Redis高性能

  • 基于内存的存储数据库,绝大部分都是纯粹内存的操作,对于内存而言读写速度非常快
  • Redis是单进程线程服务,虽然server不止一个,但是只有一个线程来处理网络请求
  • Redis采用IO多路复用,高效处理大量并发连接

跳表的底层实现(跳表的空间复杂度为O(n), 对于其查找、插入以及删除操作的时间复杂度为O(logn) ):
1、跳跃表节点实现

  1. typedef struct zskiplistNode {
  2. sds ele;
  3. double score;
  4. struct zskiplistNode *backward;
  5. struct zskiplistLevel {
  6. struct zskiplistNode *forward;
  7. unsigned int span;
  8. } level[];
  9. } zskiplistNode;
  • sds修饰的ele用于描述存储字符串类型的数据
  • score用于存储分数
  • backward是后退指针,(反向遍历跳表时使用)
  • level为柔性数组,其中包括两个元素:指向的下一个节点、以及从该节点到下一个节点的span跨度

2、跳跃表的结构

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
  • 包括跳跃表的头节点、尾节点
  • 跳跃表的长度
  • 跳跃表的高度

跳表的查询操作:

字典类型数据结构

  • 通过两个hash表来实现
  • 关键点 : 渐进式rehash的过程

Redis5.0出现的新数据类型 : Stream

Redis中String的使用

  • 单纯数据缓存功能
  • 计数功能
  • 共享session(如果将session存入后端服务器,如果在使用负载均衡的技术,那么用户刷新可能会出现需要重新登陆的问题)通过redis将用户的session进行集中管理,那么保证redis高可用即可
  • 限速功能,限制网站访问的次数等

Redis中list的应用

  • 消息队列
  • 文章列表或者是微博中的用户关注比对情况

    Redis中set的应用场景

  • 设置标签

  • 生成随机数,比如抽奖等功能
  • 另外还有社交需求(查找你对应的共同好友等功能实现)

Redis中zset的应用场景

  • 有序集合经典使用场景就是排行榜系统
  • 或者是点赞次数

缓存穿透

缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。
解决的办法如下。

  1. 对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
  2. 使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。

    缓存击穿

    缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。
    解决这个问题有如下办法。

  3. 可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。

  4. 使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
  5. 针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。

    缓存雪崩

    缓存雪崩,产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。
    解决方法:

  6. 使用快速失败的熔断策略,减少 DB 瞬间压力;

  7. 使用主从模式和集群模式来尽量保证缓存服务的高可用。

实际场景中,这两种方法会结合使用。

Reactor反应器模式

Reactor反应器模式是由Reactor反应器线程以及Handlers处理器组成
1)Reactor反应器线程的职责为 : 负责IO事件的响应,并且分发到不同的handler处理器中
2)Handler处理器的职责:非阻塞执行业务处理逻辑

单线程反应器的实现

最主要的两个函数: attach()用于将handler处理器实例绑定到selectKeys中;attachment()用于取出对应的handler处理器实例;注意:在实际编写代码时,对于不同的Runnable接口的的实现中需要使用相同的selectKeys以及serverSocket才能让反应器和handler同属一个线程中;
但是对于单线程反应器模式依旧会出现一些问题,如果某一个handler阻塞,那么之后的那些handler都不能够执行,所以在高性能应用服务场景中,单线程反应器模式使用较少;

多线程反应器的实现

多线程池Reactor反应器的演进,主要分为两个方面 :
1、通过创建线程池的方式来升级handler处理器,避免出现某一个handler发生阻塞导致之后的handler全部无法执行的问题;
2、Reactor反应器通过引入多个selector选择器,来提升选择大量通道的能力 (创建多个子反应器,每一个子反应器负责一个通道)

总体来讲,多线程池的反应器模式大致如下 :
1)负责将输入输出处理的IOHandler处理器放入独立的线程池中,这样的话业务处理线程和IO事件查询以及服务监听的反应器线程就隔离开;
2)对于处理器为多喝的CPU,将反应器现层拆分为多个子反应器线程,同时需要引入多个选择器。这样提升了通道的能力;

并发基础中的异步回调方法

1)join方法 : 一种异步阻塞机制,只要在主线程中添加一个其他线程调用join()的方法,那么主线程就会等待其他线程执行完成之后才会继续回到主线程执行工作;
2)FutureTask类 : 依旧是一种异步阻塞式的回调方法,实现了Future接口,其中Future接口主要实现了三个功能,get()获取异步调用的结果,isDone()判断是否执行完成,isCancelled()获取并发任务的取消状态,cancel()取消并发任务的执行
对于FutureTask而言,如果想要获取异步调用的结果,那么首先肯定是创建一个Callable接口,然后通过Callable接口实例得到FutureTask实例,然后再创建一个线程Thread,具体代码展示如下:

Callable<Boolean> job = new HotWaterJob();
FutureTask<Boolean> task = new FutureTask<>(job);
Thread thread = new Thread(task, "烧水——Thread");

3)Guava中的异步回调(异步非阻塞的方式来实现的)Guava中异步调用属于异步非阻塞的调用机制,主要增强:

  • 引入ListenableFuture接口,继承了Java中的Future接口,使得Java的Future异步任务能够被监控并且得到异步非阻塞执行的结果;
  • 引入新的接口FutureCallback,该接口的目的是在异步任务执行完成之后,根据异步结果完成不同的回调处理;

Guava中异步回调的方法 :

  • 首先创建一个线程池
  • 修饰线程池,变为Guava线程池
  • 将先前创建的Callable异步执行的实例,通过submit提交到线程池中,获取ListenableFuture异步任务实例
  • 创建FutureCallaback回调实例,实现其中的subccess和失败方法,并通过Future.addCallback绑定到ListenableFuture上;

4)CompletableFuture,该类是在jdk1.8中新引入的,主要学习点:

  • CompletableFuture的创建: ```java CompletableFuture completableFuture1 = CompletableFuture.supplyAsync(); //存在返回值 CompletableFuture completableFuture2 = CompletableFuture.runAsync(); //不存在返回值

get()方法用于获取结果 join()也是提供结果,并且不会抛出异常 ```

  • CompletableFuture异步以及同步的性能测试

异步执行的效率会变得很高,并且能够使用函数式编程的方式来实现

  • 已经存在Future为什么还需要引入CompletableFuture

在遇到比较复杂的异步场景之后,单纯的Future接口编程实现起来就会变得很复杂,例如,某一个异步计算的结果需要前一个异步任务的值,如果采用Future接口就需要通过轮询的方式不断判断上一个异步任务是否执行完毕;
对于CompletableFuture而言,可以通过函数式编程的方式thenApply()可以进行结果传递,另外还有一些thenAccept() thenCombine()等方式来实现

  • CompletableFuture的应用场景

对于某些IO密集型任务可以使用,IO部分交给另外的线程去完成。LogBack这些异步日志记录的实现原理就是新起一个线程去执行IO操作

  • 对CompletableFuture的使用优化