Redis 核心数据结构(二)

Redis 核心数据结构(二)

D瓜哥
本文内容对于 Redis 7+ 来说已经过时,最新实现请看下面两篇文章: Redis 核心数据结构(3) Redis 核心数据结构(4) 在上一篇文章: Redis 核心数据结构(1) 中,介绍了链表、ziplist、quicklist 数据结构。这篇文章,来介绍一下 skiplist、dict。 skiplist 跳跃表是一种有序数据结构,支持平均 O(logN)、最坏 O(N) 复杂度的节点查找;大部分情况效率可以和平衡树相媲美,实现却比平衡树简单。 跳跃表就是 Redis 中有序集合键的底层实现之一。 server.h typedef struct zskiplistNode { sds ele; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned long span; } level[]; } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist; typedef struct zset { dict *dict; zskiplist *zsl; } zset; skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。 当我们想查找数据的时候,可以先沿着跨度大的链进行查找。当碰到比待查数据大的节点时,再回到跨度小的链表中进行查找。 skiplist正是受这种多层链表的想法的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logN)。但是,存在的一个问题是:如果插入新节点后就会打乱上下相邻两层节点是 2:1 的对应关系。如果要维持,则需要调整后面所有的节点。 skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。 插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。实际上,这是 skiplist 的一个很重要的特性,这让它在插入性能上明显优于平衡树的方案。 skiplist,翻译成中文,可以翻译成“跳表”或“跳跃表”,指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。 在中间插入一个有比较高 Level 的节点,如何维护前面节点到这个节点的这些链接? 在平衡树种,如何做范围查找?先确定边界,然后其他节点怎么查找? skiplist 中 key 允许重复。 在比较时,不仅比较分数(即key),还要比较数据自身。 第一层链表是双向链表,并且反向指针只有一个。 在 skiplist 中可以很方便计算每个元素的排名。 Redis 中的有序集合(sorted set),是在 skiplist, dict 和 ziplist 基础上构建起来的: 当数据较少时,sorted set是由一个 ziplist 来实现的。其中集合元素按照分值从小到大排序。 当数据多的时候,sorted set 是由一个叫 zset 的数据结构来实现的,这个 zset 包含一个 dict + 一个 skiplist。dict 用来查询数据到分数(score)的对应关系,而 skiplist 用来根据分数查询数据(可能是范围查找)。
Redis 核心数据结构(一)

Redis 核心数据结构(一)

D瓜哥
本文内容对于 Redis 7+ 来说已经过时,最新实现请看下面两篇文章: Redis 核心数据结构(3) Redis 核心数据结构(4) Redis 目前是使用最广泛的缓存中间件。其突出特点就是支持多种常见的数据结构。对比 JDK 集合类的实现,Redis 的实现表现出很多独到之处,很多地方设计得别具匠心。下面就来简要介绍一下。 linkedlist Redis 底层也有很多地方使用到 linkedlist,并且也是双向链表。 adlist.h typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; typedef struct listIter { listNode *next; int direction; } listIter; typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned long len; } list; Redis 的 linkedlist 实现特点是: 双向:节点带有前后指针; 无环:首尾没有相连,所以没有构成环状; 链表保存了首尾指针; 多态:可以保存不同类型的值,这里成为泛型也许更符合 Java 中的语义。 Redis 在 2014 年实现了 quicklist,并使用 quicklist 代替了 linkedlist。所以,现在 linkedlist 几乎已经是废弃状态。 ziplist Redis 官方在 ziplist.c 文件的注释中对 ziplist 进行了定义: The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.
Kafka 常见面试题

Kafka 常见面试题

D瓜哥
Kafka 是由 LinkedIn 开发的一个分布式的消息系统,使用 Scala 编写,它以可水平扩展和高吞吐率而被广泛使用。Kafka 本身设计也非常精巧,有很多关键的知识点需要注意。在面试中,也常常被问到。整理篇文章,梳理一下自己的知识点。 架构设计问题 Kafka 整体架构如下: 图 1. Kafka 架构 Kafka 架构分为以下几个部分 Producer:消息生产者,就是向 Kafka Broker 发消息的客户端。 Consumer:消息消费者,向 Kafka Broker 取消息的客户端。 Topic:可以理解为一个队列,一个 Topic 又分为一个或多个分区。 Consumer Group:这是 Kafka 用来实现一个 Topic 消息的广播(发给所有的 Consumer)和单播(发给任意一个 Consumer)的手段。一个 Topic 可以有多个 Consumer Group。 Broker:一台 Kafka 服务器就是一个 Broker。一个集群由多个 Broker 组成。一个 Broker 可以容纳多个 Topic。 Partition:为了实现扩展性,一个非常大的 Topic 可以分布到多个 Broker上,每个 Partition 是一个有序的队列。Partition 中的每条消息都会被分配一个有序的id(offset)。将消息发给 Consumer,Kafka 只保证按一个 Partition 中的消息的顺序,不保证一个 Topic 的整体(多个 Partition 间)的顺序。 Offset:Kafka 的存储文件都是按照 offset.Kafka 来命名,用 offset 做名字的好处是方便查找。例如你想找位于 2049 的位置,只要找到 2048.Kafka 的文件即可。当然 the first offset 就是 00000000000.Kafka。
深入剖析 Spring 核心数据结构:BeanFactory

深入剖析 Spring 核心数据结构:BeanFactory

D瓜哥
在 深入剖析 Spring 核心数据结构:BeanDefinition 中,介绍了 BeanDefinition。网上很多文章介绍 BeanDefinition 的 API,D瓜哥却要反其道而行之,从内部属性来分析一下。下面我们开始。 继承体系 Spring 非常好地遵循了面向对象的设计原则:面向接口编程。不放过任何可以提取出成接口的机会。虽然感觉似乎增加了类的继承关系,增加了一点的复杂度。但是,却带来了非常好的可扩展性。而 BeanFactory 的继承体系就是一个非常典型的例子。我们来看一下它的继承体系: 图 1. BeanFactory 继承体系 AliasRegistry:别名注册器。Spring 中,别名注册相关的功能就是从这里实现的。 SimpleAliasRegistry:别名注册器的一个简单实现,从内部属性可以看出,它是把别名映射信息存到一个 Map 中了。 DefaultSingletonBeanRegistry:默认的单例 Bean 注册器,从内部属性来说,也是基于 Map 实现的。 FactoryBeanRegistrySupport: FactoryBean 注册器。 SingletonBeanRegistry:单例 Bean 注册器。 BeanDefinitionRegistry: BeanDefinition 注册器。 BeanFactory:容器的基类。 ListableBeanFactory:在基本容器基础上,增加了遍历相关功能。 HierarchicalBeanFactory:在基本容器基础上,增加了父子上下级容器关联。 AutowireCapableBeanFactory:在基本容器基础上,增加了自动注入功能。 ConfigurableBeanFactory:对容器增加可配置性,比如父级容器、ClassLoader、TypeConverter 等。 ConfigurableListableBeanFactory:可配置可遍历容器。 AbstractBeanFactory:容器的抽象实现类,实现了容器的基础功能。 AbstractAutowireCapableBeanFactory:带自动装配功能的抽象容器类。 DefaultListableBeanFactory:这是 Spring 内部使用的默认容器实现。也是 Spring 中最重要的一个类。 核心属性 Registry Map<String, String> aliasMap = new ConcurrentHashMap<>(16):别名到 Bean 名称的映射。 Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256):Bean 名称到单例 Bean 的映射。可以理解成,这就是所谓的容器。 Map<String, Object> earlySingletonObjects = new HashMap<>(16):Bean 到“未成熟”单例 Bean 的映射。该 Bean 对象只是被创建出来,但是还没有注入依赖。在容器解决循环依赖时,用于存储中间状态。 Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16):Bean 名称到 Bean 的 ObjectFactory 对象的映射,在容器解决循环依赖时,用于存储中间状态。 关于这三个属性的进一步说明,请移步: 源码剖析 Spring 循环依赖。
深入剖析 Spring 核心数据结构:BeanDefinition

深入剖析 Spring 核心数据结构:BeanDefinition

D瓜哥
林纳斯·托瓦兹(Linus Torvalds)说:“我从心底认为,优秀的程序员与平庸的程序员之间的区别,是在于认为自己的代码重要还是数据结构更加重要。平庸的程序员眼里只有代码,优秀的程序员则关注数据结构及之前的关系。” 也许很多人觉得 Spring 神秘莫测,但是如果了解了它的核心数据结构,很多问题迎刃而解。 Spring 中两个数据结构最核心:① BeanDefinition,用于表示 Bean 的定义;② BeanFactory,用于表示整个 IoC 容器。 在前面文章 Spring Bean 生命周期概述中,介绍了 Spring Bean 的生命周期。不知道大家有没有思考过 Spring 在内部是如何表示一个 Bean 的?本篇文章,就来聊一聊 BeanDefinition 问题 使用 Spring 时,尤其是使用 XML 配置的时候,也许我们会这样的问题: Bean 怎么表示? Bean 的依赖怎么表示? init-method 方法怎么存储? Bean 的一些属性,比如 lazy-init 等,怎么表示? Bean 构造函数的参数怎么存储? …​ Java 也有类似的问题,比如怎么表示一个类?Java 通过反射 API 来解决这个问题: Class Method Field Constructor Annotation 但是,为什么 Spring 还要自己定义一套呢?主要原因是 Java 反射 API 不满足 Spring 的需求,比如,它没办法表示哪些类是 SCOPE_SINGLETON,哪些类是 SCOPE_PROTOTYPE。 另外,Spring 的 Bean 抽象也并不是完全自定义的,它是基于 Java 反射 API 又增加了自定义功能,其核心 API 就是 BeanDefinition。下面,我们来仔细看一下它的继承体系以及内部核心属性。
题解:找到二叉搜索树中两个错误的节点

题解:找到二叉搜索树中两个错误的节点

D瓜哥
题目描述 一棵二叉树原本是二叉搜索树,但是其中有两个节点调换了位置,使得这棵二叉树不再是二叉搜索树,请按升序输出这两个错误节点的值。(每个节点的值各不相同) 思路分析 一棵二叉搜索树,如果是中序遍历,那么,其序列正好是一个升序序列。如果序列中间出现了降序,那么就是树中错误的节点。这里有两种情况需要考虑: 如果两个节点正好相邻,那么降序的两个节点就是题目要求的节点。 如果两个节点不相邻,就会出现两次降序。很容易想到,“大”的元素跑前面,“小”的元素跑后面。在进行比较的时候,第一个降序时,前面大的元素是错误节点;第二次降序时,则是后面小的元素是错误节点。 有了上面的分析,就可以写代码了。 解题代码 中序遍历,可以用栈,也可以使用 Morris 遍历。前面正好学习了一下 Morris(详情请看: 神奇的 Morris 树遍历),借此机会练练手: /** * 基于 Morris 的中根遍历 * * @author D瓜哥 · https://www.diguage.com */ public TreeNode[] getErrs(TreeNode head) { TreeNode[] result = new TreeNode[2]; TreeNode curr = head; TreeNode mostRight = null; TreeNode prior = null; while (curr != null) { mostRight = curr.left; if (mostRight != null) { while (mostRight.right != null && mostRight.right != curr) { mostRight = mostRight.right; } if (mostRight.right == null) { mostRight.right = curr; curr = curr.left; continue; } else { mostRight.right = null; } } if (prior != null) { if (prior.val > curr.val) { if (result[0] == null) { result[0] = curr; result[1] = prior; } else { result[0] = curr; } } } prior = curr; curr = curr.right; } return result; } 得益于 Morris 的优秀本质,这个解法的时间复杂度是: O(n),空间复杂度是: O(1)。
Spring AOP 处理流程概述

Spring AOP 处理流程概述

D瓜哥
AOP 是 Spring 框架的最核心的两个功能之一,在前面的 Spring 启动流程概述 和 Spring Bean 生命周期概述 两篇文章中,分别介绍了 Spring 启动过程和 Spring Bean 的生命周期,对 IoC 有了一个细致介绍。这里来细致分析一下 Spring AOP 的实现原理和处理流程。 基本概念 先来了解几个基本概念,D瓜哥私以为这些概念是 AOP 中最核心的内容,了解了基本概念,可以说基本上掌握了一半的 AOP 内容。 学习概念最权威的地方,当然就是官方文档。所以,这些概念可以在 Spring Framework Documentation: AOP Concepts 中看到最权威的介绍。 Join point(连接点): 所谓的连接点是指那些被拦截到的点。在 Spring 中,连接点指的是方法,因为 Spring 只支持方法类型的连接点。在 Spring 中,使用 Pointcut(切入点): 所谓的切入点,是指要对哪些 Join point(连接点) 进行拦截的定义。如果 Join point(连接点) 是全集,那么 Pointcut(切入点) 就是被选中的子集。写 AOP 代码的时候,一般是用 Pointcut(切入点) 表达式进行对 Join point(连接点) 进行选择。 Advice(通知/增强): 所谓的通知就是指拦截到 Join point(连接点) 之后所要做的事情。通知根据作用位置不同,又细分为: Before advice(前置通知): 在 Join point(连接点) 之前运行的通知。这种通知,不能阻止执行流程继续到 Join point(连接点)。 After returning advice(后置通知): 在 Join point(连接点) 之后运行的通知。当然,如果在 Join point(连接点) 执行过程中,抛出异常,则可能就不执行了。 After throwing advice(异常通知): 方法抛出异常后,将会执行的通知。 After (finally) advice(最终通知): 无论如何都会执行的通知,即使抛出异常。 Around advice(环绕通知): 围绕在 Join point(连接点) 的通知,方法执行前和执行后,都可以执行自定义行为。同时,也可以决定是返回 Join point(连接点) 的返回值,还是返回自定义的返回值。
Spring Bean 生命周期概述

Spring Bean 生命周期概述

D瓜哥
在 Spring 启动流程概述 中,分析了 Spring 的启动流程。本文就来说明一下 Spring Bean 整个生命周期。如果有不清楚的地方,可以参考上文的“附录:启动日志”。 直接上图:Spring Bean 生命周期流程图。内容较多,图片文字偏小,请放大看(矢量图,可以任意放大): 图 1. Spring Bean 生命周期流程图 下面是文字说明。 Bean 生命周期简述 调用 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation,主要是判断 AnnotationAwareAspectJAutoProxyCreator 是否可以生成代理。 调用构造函数 调用 MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition,主要是通过 CommonAnnotationBeanPostProcessor、 AutowiredAnnotationBeanPostProcessor 收集依赖信息。 InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation,这步什么也没做。 调用 InstantiationAwareBeanPostProcessor#postProcessProperties,主要是完成依赖注入。 调用 AutowiredAnnotationBeanPostProcessor#setBeanFactory,注入 BeanFactory 等相关信息。 调用 BeanPostProcessor#postProcessBeforeInitialization,主要是注入 ApplicationContext 等相关信息。 调用 InitializingBean#afterPropertiesSet、 init-method 方法 调用 BeanPostProcessor#postProcessAfterInitialization,主要是生成 AOP 代理类。 Bean 生命周期详解 从 getBean() 方法获取 Bean 时,如果缓存中没有对应的 Bean,则会创建 Bean,整个流程如下: InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation — 目前有如下四个: ImportAwareBeanPostProcessor — 继承父类实现,无所事事。 AnnotationAwareAspectJAutoProxyCreator — 继承父类实现,判断是否属于基础切面类,如果有指定的 Target 则生成代理。 CommonAnnotationBeanPostProcessor — 无所事事。 AutowiredAnnotationBeanPostProcessor — 继承父类实现,无所事事。
Spring 启动流程概述

Spring 启动流程概述

D瓜哥
对于 Spring 启动流程和 Bean 的生命周期,总有一些小地方搞的不是很清楚,干脆直接通过修改代码增加日志输出,使用断点单步调试,把整个流程捋顺了一点点的。 除了加载配置文件或者基础配置类外,Spring 的启动过程几乎都被封装在 AbstractApplicationContext#refresh 方法中,可以说弄清楚了这个方法的执行过程,就摸清楚了 Spring 启动全流程,下面的流程分析也是以这个方法为骨架来展开的。 流程概要 下面完整流程有些太复杂,所以,提炼一个简要的过程,方便糊弄面试官,哈哈哈😆 创建容器,读取 applicationContext.register(Config.class) 指定的配置。 准备 BeanFactory,注册容器本身和 BeanFactory 实例,以及注册环境配置信息等。 执行 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 注册 BeanDefinition。有三点需要注意: 目前只有一个 ConfigurationClassPostProcessor 实现类,Spring 中大量的 Bean 都是在这一步被该类注册到容器中的。 执行顺序是 ① PriorityOrdered ② Ordered ③ 普通的顺序来执行 在执行上一步时,如果发现注册了 BeanDefinitionRegistryPostProcessor 类型的 Bean,就会在循环里继续调用 postProcessBeanDefinitionRegistry 方法。MyBATIS 和 Spring 整合的 MapperScannerConfigurer 类就是在这一步执行的。 执行 BeanFactoryPostProcessor#postProcessBeanFactory 方法。目前只有一个 ConfigurationClassPostProcessor 实现类。 注册 CommonAnnotationBeanPostProcessor 和 AutowiredAnnotationBeanPostProcessor 为 BeanPostProcessor。 注册 ApplicationEventMulticaster,用于广播事件的。 注册 ApplicationListener 预加载以及注册所有非懒加载的 Bean 启动时序图 Spring 启动流程的时序图如下:
Spring 扩展点实践:整合 MyBATIS

Spring 扩展点实践:整合 MyBATIS

D瓜哥
在上一篇文章 Spring 扩展点概览及实践 中介绍了 Spring 内部存在的扩展点。学以致用,现在来分析一下 Spring 与 MyBATIS 的整合流程。 示例程序 为了方便分析源码,先根据官方文档 mybatis-spring – MyBatis-Spring | Getting Started 搭建起一个简单实例。 数据库方面,直接使用功能了 MySQL 示例数据库: MySQL : Employees Sample Database,需要的话,自行下载。 package com.diguage.truman.mybatis; import com.mysql.cj.jdbc.Driver; import com.zaxxer.hikari.HikariDataSource; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.Test; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; /** * @author D瓜哥, https://www.diguage.com/ * @since 2020-05-29 17:11 */ public class MybatisTest { @Test public void test() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(Config.class); context.refresh(); EmployeesMapper employeesMapper = context.getBean(EmployeesMapper.class); Employees employees = employeesMapper.getById(10001); System.out.println(employees); } @org.springframework.context.annotation.Configuration @MapperScan(basePackages = "com.diguage.truman.mybatis") public static class Config { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setDriverClassName(Driver.class.getName()); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/employees?useUnicode=true&characterEncoding=utf-8&autoReconnectForPools=true&autoReconnect=true"); return dataSource; } @Bean public SqlSessionFactoryBean sqlSessionFactory(@Autowired DataSource dataSource) { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); Configuration configuration = new Configuration(); configuration.setMapUnderscoreToCamelCase(true); factoryBean.setConfiguration(configuration); return factoryBean; } } } EmployeesMapper package com.diguage.truman.mybatis; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; /** * @author D瓜哥, https://www.diguage.com/ * @since 2020-05-29 17:23 */ public interface EmployeesMapper { @Select("SELECT * FROM employees WHERE emp_no = #{id}") Employees getById(@Param("id") Integer id); } Employees package com.diguage.truman.mybatis; import java.util.Date; /** * @author D瓜哥, https://www.diguage.com/ * @since 2020-05-29 17:24 */ public class Employees { Integer empNo; Date birthDate; String firstName; String lastName; String gender; Date hireDate; @Override public String toString() { return "Employees{" + "empNo=" + empNo + ", birthDate=" + birthDate + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", gender='" + gender + '\'' + ", hireDate=" + hireDate + '}'; } } 整个实例代码中,只有 @MapperScan(basePackages = "com.diguage.truman.mybatis") 这个注解和 MyBATIS 的配置相关,我们就从这里开始吧。