开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

分享一个让我进入阿里中间件的个人项目 来自中间件的邀请 ir

发表于 2019-11-11

作者: vangoleo
官网: www.vangoleo.com/iris-java/

背景

时光荏苒,进入阿里中间件团队已经快两年时间了。这期间,有幸参与了第四届中间件性能挑战赛的题目组,筹备了以“Dubbo Mesh”为主题的初赛题;和团队一起开展了Dubbo线下meetup活动;将阿里多年双11积累的中间件基础设施最佳实践和方法论,通过阿里云的商业化产品,为广大开发者和企业提供服务。很庆幸能有这样一段难忘的经历。回想起来,能进入中间件团队,和我当初的一个Github项目还有关系。今天把该项目分享给大家。

Q: 什么是中间件团队?
A: 阿里巴巴中间件技术部,是世界顶尖的Java技术团队之一,起源于淘宝平台架构组,是跟随着阿里电商业务和双十一成长起来的技术团队,解决复杂的业务场景、飞速的业务增长、高并发的大促洪峰、层出不穷的稳定性问题。产品包括高分布式RPC服务框架、高可靠分布式消息中间件、分布式数据层、海量数据存储、实时计算、系统性能优化、架构高可用等几大领域的多个产品,这些产品支撑阿里巴巴集团(淘宝、天猫、聚划算、1688、菜鸟)的所有交易和非交易业务系统,安然平稳度过双十一917亿交易成交的挑战。我们开源的中间件组件Dubbo、Rocketmq、Nacos、tengine、Seata等都被很多企业和个人在使用。

来自中间件的邀请

2017年的时候,我带领团队对后端架构进行了微服务重构。选型时使用了Dubbo框架。得益于Dubbo的高性能,使用简单和高扩展性,微服务改造很顺利,公司的业务也越来越稳定。我对Dubbo也产生了浓厚的兴趣,希望可以更深入地了解这个优秀的RPC框架。我研究了下Dubbo的源码,自己从零开始编写了一个mini版的Dubbo。
恰好时值阿里又重启了Dubbo项目,且成为Apache的孵化项目(编写文本时已正式成为Apache项目)。Dubbo新的官网有一个“Wanted: who’s using dubbo”页面,我也留下了自己的信息,来给Dubbo点个赞。其中包含了mini版Dubbo的项目地址。

其实是很随意的一个举动,没想到会发生后面的故事。一个小时后,我收到了一封邮件:

这是一封来自中间件团队Dubbo负责人的邮件。当时感觉挺意外的,也很欣喜。中间件团队一直是我认为技术和影响力都很强的团队,如果可以加入该团队,是一个很好的机会。

于是接下来就是例行的投简历,面试流程。要吐槽下阿里的面试流程,前后历时快两个月了,一共有五轮,真的是持久战呀。面试的时候,面试官问了一些关于mini Dubbo的问题。结果还不错,很侥幸的通过了面试,正式加入中间件的Dubbo团队。后来听我的老板说,当初是因为对我的mini Dubbo项目感兴趣,才有了面试邀约。
iris
====

mini版Dubbo的项目地址为:github.com/vangoleo/ir…。我给它取名为iris。

iris是一个轻量级,微内核加插件机制,基于Java的RPC框架。提供服务注册,发现,负载均衡,支持API调用,Spring集成和Spring Boot starter使用。

有如下特性:

  • 网络通信: Netty4。
  • 注册中心: 可扩展,已支持etcd。
  • 动态代理: byte-buddy。
  • 序列化: Protobuff(Protostuff)。
  • 可以脱离Spring,提供API调用。自己实现了IoC容器。
  • 集成Spring,提供XML,Java配置。
  • 提供Spring Boot Starter(开发该项目时,Dubbo官方还不支持Spring Boot Starter)。
  • 提供SPI机制,实现微内核加插件的架构。实现可扩展,开发者可以为iris开发组件,以插件的形式集成到iris中。插件的加载使用另一个微容器框架见coco项目。该项目fork于阿里的cooma。

说明:iris完全是我个人学习的项目,麻雀虽小,五脏俱全,涵盖了RPC框架的基本功能。它只是实现了从0到1,但是从1到100还有很多的事情需要去做。

如何使用

iris支持以下使用方式:

  • 原生API形式,不依赖Spring,非Spring项目也可以使用。
  • Spring配置方式,和Spring很好的集成。
  • Spring Boot配置方式,提供了一个spring boot starter,以自动配置,快速启动。

API使用

Iris核心代码不依赖Spring,可脱离Spring使用。

第一步:启动etcd注册中心
编写一个接口IHelloService

1
2
3
复制代码public interface IHelloService {
String hello(String name);
}

第二步:编写一个IHelloService的实现

1
2
3
4
5
6
复制代码public class HelloService implements IHelloService {
@Override
public String hello(String name){
return "Hello, " + name;
}
}

第三步:启动Server

1
2
3
4
5
复制代码IRegistry registry = new EtcdRegistry("http://127.0.0.1:2379");
RpcServer server = new RpcServer(registry)
.port(2017)
.exposeService(IHelloService.class,new HelloService());
server.run();

第四步:启动client

1
2
3
4
复制代码RpcClient client = new RpcClient(registry);
IHelloService helloService = client.create(IHelloService.class);
String s = helloService.hello("leo");
System.out.println(s); // hello, leo

第五步:试着停止server
因为服务没有provider,client报错找不到provider

第六步:启动server

Server启动后,会去etcd注册中心注册服务,client端马上正常工作。

Spring配置方式

第一步:编写服务提供者
服务提供者,使用自定义注解@Service来暴露服务,通过interfaceClass来指定服务的接口。该@Service注解是iris提供的,并非Spring的注解。

1
2
3
4
5
6
7
复制代码@Service(interfaceClass = IHelloService.class)
public class HelloService implements IHelloService {
@Override
public String hello(String name) throws Exception {
return "hello" + name;
}
}

第二步:编写服务消费者
服务使用者,通过@Reference来引用远程服务,就像使用本地的SpringBean一样。背后的SpringBean封装和Rpc调用对开发者透明。使用体验和Dubbo是一样的。

1
2
3
4
5
6
7
8
9
复制代码public class Baz {

@Reference(interfaceClass = IHelloService.class)
private IHelloService helloService;

public void hello(String name) throws Exception {
System.out.println(helloService.hello(name));
}
}

第三步:配置Spring Bean
配置服务提供者,本例子使用XML配置,使用Java Code配置也可以。

1
2
3
4
5
6
7
8
9
10
11
复制代码<bean id="registry" class="com.leibangzhu.iris.registry.EtcdRegistry">
<constructor-arg name="registryAddress" value="http://127.0.0.1:2379"></constructor-arg>
</bean>

<bean id="server" class="com.leibangzhu.iris.server.RpcServer">
<constructor-arg name="registry" ref="registry"></constructor-arg>
</bean>

<bean id="serviceAnnotationBeanPostProcessor" class="com.leibangzhu.iris.spring.ServiceAnnotationBeanPostProcessor"></bean>

<bean id="helloService" class="com.leibangzhu.iris.spring.HelloService"></bean>

第四步:配置服务消费者,本例子使用XML配置,使用Java Code配置也可以。

1
2
3
4
5
6
7
8
9
10
11
复制代码<bean id="registry" class="com.leibangzhu.iris.registry.EtcdRegistry">
<constructor-arg name="registryAddress" value="http://127.0.0.1:2379"></constructor-arg>
</bean>

<bean id="client" class="com.leibangzhu.iris.client.RpcClient">
<constructor-arg name="registry" ref="registry"></constructor-arg>
</bean>

<bean id="referenceAnnotationBeanPostProcessor" class="com.leibangzhu.iris.spring.ReferenceAnnotationBeanPostProcessor"></bean>

<bean id="foo" class="com.leibangzhu.iris.spring.Baz"></bean>

Spring Boot配置

使用原生的Spring配置还是有些繁琐,可以使用Spring Boot来获得更好的开发体验。
第一步:编写服务提供者

1
2
3
4
5
6
7
复制代码@Service(interfaceClass = IHelloService.class)
public class HelloService implements IHelloService {
@Override
public String hello(String name) throws Exception {
return "Hello, " + name + ", from com.leibangzhu.iris.springboot.HelloService";
}
}

第二步:编写服务消费者

1
2
3
4
5
6
7
8
9
10
复制代码@Component
public class Foo {

@Reference(interfaceClass = IHelloService.class)
private IHelloService helloService;

public String hello(String name) throws Exception {
return helloService.hello(name);
}
}

第三步:在application.properties文件中配置服务提供者

1
2
3
4
5
复制代码iris.registry.address=http://127.0.0.1:2379

iris.server.enable=true
iris.server.port=2017
iris.annotation.package=com.leibangzhu.iris.springboot

第四步:在application.properties文件中配置服务消费者

1
2
复制代码iris.registry.address=http://127.0.0.1:2379
iris.client.enable=true

使用SpringBoot时,不许再手动配置相关的spring bean,Iris提供的spring boot starter会自动配置好这些spring bean。

为什么取名iris

iris取名于梵高的画鸢尾花。我自己比较喜欢绘画,梵高是我比较喜欢的画家,所以用梵高的鸢尾花为项目取名。

为什么会喜欢梵高?
其实吸引我的不是画家梵高,而是那个虔诚,善良又狂热的基督教徒梵高。引用我多年前的一条朋友圈:

最近每天晚上睡觉前都会听一段梵高传。其实之前并不了解梵高,很庆幸可以听到蒋勋老师的这段梵高传,可以了解这样精彩的一个生命。梵高有生之年只卖出过一幅画,还是以很低的价格,然而在他死后,他的画却成为最伟大的艺术作品。为什么梵高的向日葵会燃烧的如此热烈,很多人会说是因为他对艺术的热爱,甚至有人说是因为梵高视觉上的残疾。其实不是这样的。在他短暂的37年的生命里,他作为画家的身份其实只有生命中的最后4年,梵高一生更重要的角色是一个虔诚,狂热的基督教徒。比利时矿工时的梵高,当面对着一群衣衫褴褛,骨瘦嶙峋的矿工,梵高显得不安了,作为一个基督教徒,不应该看到这样的一群人们而无所作为,作为一个基督徒,应该看到人世间的苦难,然后能够去承担它,这才叫做救赎。耶稣不是因为救赎才来到人间的吗,耶稣不是因为救赎才被钉在十字架上的吗。如果生命中看不到救赎的部分,耶稣基督存在的意义又是什么呢。他脱掉了身上华丽的黑袍,摘掉了白领巾,拿起铲子,竟和他们一起下到矿坑里去。因为面对这样的一群人,华丽的布道语言根本不能帮助他们,只有去真正的感受他们的生活,才可以帮助,救赎这些穷困的人们。梵高还把教堂的椅子拆掉,来安置矿难中受伤的矿工,把自己的食物全部分给矿工,这样的一个有着深沉的人道主义关怀的教徒,收到的是教会的解雇书,教会认为一个牧师应该穿着华丽的黑袍,讲着非常冠冕堂皇的布道语言,梵高没有维护教会的尊严。那是梵高一生中收到的最大的打击。在梵高濒临绝望的时候,拯救他的竟然是艺术,是画画。是怎样的画笔可以描绘出阿尔如此热烈的向日葵,又是怎样的精神上的折磨,会让他割掉自己的耳朵,梵高的那么多自画像,预示着什么?又是怎样的一个生命,可以在精神病院,被囚禁的那间小屋,创作出了他一生最伟大的作品starry night。梵高最后一幅画麦田里的乌鸦,是否已预言了他生命的终结。这就是那个被叫做文森特 - 威廉 - 梵高的生命。

我的github账号vangoleo其实就是vangogh(梵高) + leo(我的英文名)的组合。

最后附上我自己的几幅画,不知道大家能不能认出来他们^_^

leehom

全职猎人

全职猎人

follow me

本文由 www.vangoleo.com 发布

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

快手面经篇一,据说看了面试通过率提升50%

发表于 2019-11-11

写给正在找工作的你

都说金三银四,对于找工作的人来说,因为每年的三月或四月是不少互联网公司的年终季,不少人都是拿到年终奖后不满意,或者感觉职业发展受限,之后跑路。这样不少部门因为人员流动,就会有hc空缺出来。
==这里要说的是每年3、4月份确实是hc最多的季节,但同时是跳槽旺季,竞争大,你要想找到好的坑位,那就需要绝对的实力才行。==
相对来说,其实年底是个好时候,俗话说,铁打的营盘流水的兵,互联网的阵地上不少岗位是常年招人,常年缺人,当然hc并不富裕,但是年底的时候,看机会的人也少。毕竟不少人还是很在意“年终奖”的嘛。所以说,年底跳槽你可能会损失一部分年终奖,但换工作的竞争性相对来说也会少很多,竞争的人少了嘛,说不定你就可以凭“运气的实力”脱颖而出呢?

快手面试

算法

面试官很亲切,说Excel表用过吧,Excel表中的编号一般是这样的,A….Z AA…AZ BA…BZ,分别对应数字0…25 26….51 52…77,类比做数字映射,给出一个字符串,求映射的结果。

分析 这个题目其实很基础,可以理解为是以26为基准的进制转换,一个for循环,除了末尾的字符直接加到结果上之外,其他的字符位-‘A’+1的结果乘以26*(该字符位置与末尾的差值)。做这种题目一定要先思考,自己手动实现一下。
如果想看具体代码答案,可以扫码关注【程序员之道】,后台回复“快手列转换”。

在这里插入图片描述

第二个算法,就稍微有一点偏了,如何实现redis的分布式锁。

如果没有接触过高并发,或者没有使用过redis作为分布式锁,那这这个算法肯定是写不出来的,而且像这种算法,一般来说可能也就是让讲讲思路。具体实现确实有点难。
关于分布式锁,其实是有几个坑的:

  1. 加锁,必须设置过期时间(防止释放锁失败,有过期时间,锁可以自动释放)。且加锁和设置过期时间必须为原子操作。否则,如果加锁成功,但设置过期时间时客户端崩溃,那设置过期时间就失败了。
  2. 加锁和释放锁必须是同一个客户端。用唯一id来标志。
  3. 释放锁时,判断锁是否属于自己及释放锁必须是原子操作。

思考了这些,你能写成正确的加锁,解锁方式吗?具体的坑及正确的加解锁方式,关注【程序员之道】,后台回复“redis分布式锁”。

基础

  • mysql索引怎么建立,查询语句select * from T where a=”a” and b=”b” and c=”c”,与select * from T where a=”a” and c=”c” and b=”b”执行有什么区别吗?建议索引遵循什么原则?
  • 要点:*
1
2
3
4
5
6
7
8
9
10
复制代码    (1)尽量减少like,但不是绝对不可用,”xxxx%” 是可以用到索引的
(2)表的主键、外键必须有索引
(3 谁的区分度更高(同值的最少),谁建索引,区分度的公式是count(distinct(字段))/count(*)
(4)单表数据太少,不适合建索引
(5)where,order by ,group by 等过滤时,后面的字段最好加上索引
(6)如果既有单字段索引,又有这几个字段上的联合索引,一般可以删除联合索引;
(7)联合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
(8)联合索引: mysql 从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index(a,b,c). 可以支持 a|a,b|a,b,c 3种组合进行查找,但不支持 b,c 进行查找.当最左侧字段是常量引用时,索引就十分有效。
(9)前缀索引: 有时候需要索引很长的字符列,这会让索引变得大且慢。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于覆盖索引 Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。
(10)NULL会导致索引形同虚设

=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

  • redis里有哪些数据结构,都用过什么?redis里Sorted Set怎么用,需要传什么参数?
  • 要点:*
    redis的数据结构:String,Hash、List、Set、Sorted Set,用过哪些就说哪些就行了,没用过的,估计你根据名词也能大概猜出是什么。
    面试官问Sorted Set大概是你听到你说了Sorted Set,所以问一下你命令,看你是不是真的知道啊,不知道的话,这下没法蒙混过关了吧。有序集合,设置每个key的时候需要传入一个score参数。具体命令zadd key score value。还有一些其他的命令google学习一下吧!
  • java volatile干什么用的。public int incrment() { count++},两个线程同时访问是否有问题,count如果用volatile修饰呢?
  • 要点:*
    volatile主要是保证多线程访问时的可见性。我们知道计算机为了提高访问内存的速度,引入了工作内存和主内存的概率,多线程访问数据时,访问的是工作内存的数据,各个线程之间的工作内存是分别隔离的。这就可能导致同一个变量,由于工作内存的存在,在不同线程“看到的值”是不一样的。但volatile关键字,强制了各线程读取变量时必须从主内存读取,同时对变量的修改也直接刷新到主内存,这样就保证了同一变量修改的同时可以立刻被其他线程“看到”。这里面使用了“内存屏障”的技术。
    对于count++,操作系统执行时,并不是一个原子操作,分为三步:1)将count变量load到内存。2)执行count+1。3)将结果存入内存。非原子性操作,任何一个步骤执行的时候,都可能被其他线程打断,所以多线程执行时会有问题。
    使用volatile修饰也是不可以的,因为始终不是原子操作,也只是保证可见性而已,原子性的问题无法解决。
  • jvm里内存分配什么样的,分别用来干什么?
  • 要点:*
    JVM内存分配几乎是每个java开发人员的面试必考点,单纯这部分的内容都够写几个篇章的了。这里只是简单的介绍一下。
    JVM内存分为年轻代和老年代,其中年轻代又分为S0、S1、Eden区,JVM采用分代垃圾回收算法,因为这样才能更充分的利用年轻代和老年代的对象特点,最大化的提高垃圾回收效率。
  • 类对象定义后分配在年轻代。
  • 大对象或大数组直接分配在老年代。

常见的垃圾回收算法有复制算法、标记清除、标记整理,然后又引出不同的垃圾回收器,垃圾回收器的迭代是不断发现问题并优化的过程,新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge;老年代收集器使用的收集器:Serial Old、Parallel Old、CMS。然后结合自己的理解再说一下!

  • jvm的栈是做什么,为什么有堆又有栈,只使用堆可以吗?
  • 要点:*
    JVM的栈是线程私有的,一些基本变量都是存储在栈中的,Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
    为什么有了堆之后还要有栈?栈的存在可以说是为了解决递归调用的问题。如果只有堆内存,那就不会有递归调用了。
  • 分布式自增id怎么实现,如果用redis实现,怎么保证与数据库的一致性?
    分布式自增id一般使用MySQL的自增id、redis的incr函数,还有比较经典的雪花算法。
    MySQL自增id受数据库访问速度的限制,在分布式使用时qps不大。
    使用redis产生自增id,就要防止redis崩溃的可能性,一般在MySQL或hbase中记录当前最大的value值。或者如果你设计的是一个聊天室,那肯定是有持久化存储当前聊天室的最大seqId,如果redis集群出现崩溃,从持久化存储的地方取出最大seqId然后自增即可。
  • ArrayList,LinkedList有什么区别,分别什么时候使用?
    ArrayList的底层实现是数组,数组的扩容是不断通过复制来完成的,所以存储的数据容量不断发生变化时,ArrayList的性能是比较差的。使用ArrayList时一般都是预知数据的最大容量。如果能直接使用数组,那使用数组当然是最好的了。
    LinkedList的底层实现是链表,发生数据扩容时,性能较好,但同容量情况下占用的空间比ArrayList要大。对于数据频繁扩容的情况,推荐使用LinkedList。

面试的内容还有很多,限于篇幅问题,在下一篇介绍。
程序员的小伙伴们,觉得自己孤单么,那就加入公众号[程序员之道],一起交流沟通,走出我们的程序员之道!

扫码加入吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security 实战干货: RBAC权限控制

发表于 2019-11-11

rbac.png

  1. 前言

欢迎阅读 Spring Security 实战干货系列文章 。截止到上一篇我们已经能够简单做到用户主体认证到接口的访问控制了,但是依然满足不了实际生产的需要。 如果我们需要一个完整的权限管理系统就必须了解一下 RBAC (Role-Based Access Control 基于角色的访问控制) 的权限控制模型。

  1. 为什么需要 RBAC?

在正式讨论 RBAC 模型之前,我们要思考一个问题,为什么我们要做角色权限系统? 答案很明显,一个系统肯定具有不同访问权限的用户。比如付费用户和非付费用户的权限,如果你是 QQ音乐的会员那么你能听高音质的歌曲,如果不是就不能享受某些便利的、优质的服务。那么这是一成不变的吗?又时候为了流量增长或者拉新的需要,我们又可能把一些原来充钱才能享受的服务下放给免费用户。如果你有了会员等级那就更加复杂了,VIP1 跟 VIP2 具有的功能肯定又有所差别了。主流的权限管理系统都是 RBAC 模型的变形和运用,只是根据不同的业务和设计方案,呈现不同的显示效果。下图展示了用户和角色以及资源的简单关系:

rbacflow.png

那为什么不直接给用户分配权限,还多此一举的增加角色这一环节呢?当然直接给用户具体的资源访问控制权限也不是不可以。只是这样做的话就少了一层关系,扩展性弱了许多。如果你的系统足够简单就不要折腾 RBAC 了,怎么简单就怎么玩。如果你的系统需要考虑扩展性和权限控制的多样性就必须考虑 RBAC 。如果你有多个具有相同权限的用户,再分配权限的时候你就需要重复为用户去 Query (查询) 和 Add (赋予) 权限,如果你要修改,比如上面的 VIP1 增加一个很 Cool 的功能,你就要遍历 VIP1 用户进行修改。有了角色后,我们只需要为该角色制定好权限后,将相同权限的用户都指定为同一个角色即可,便于权限管理。对于批量的用户权限调整,只需调整该用户关联的角色权限,无需遍历,既大幅提升权限调整的效率,又降低了漏调权限的概率。这样用户和资源权限解除了耦合性,这就是 RBAC 模型的优势所在。

  1. RBAC 模型的分类

RBAC 模型可以分为:RBAC0、RBAC1、RBAC2、RBAC3 四种。其中 RBAC0 是基础,其它三种都是在 RBAC0 基础上的变种。大部分情况下,使用 RBAC0 模型就可以满足常规的权限管理系统设计了。不过一定不要拘泥于模型,要以业务需要为先导。接下来简单对四种模型进行简单的介绍一下。

3.1 RBAC0

RBAC0 是基础,定义了能构成 RBAC 权限控制系统的最小的集合,RBAC0 由四部分构成:

  • 用户(User) 权限的使用主体
  • 角色(Role) 包含许可的集合
  • 会话(Session)绑定用户和角色关系映射的中间通道。而且用户必须通过会话才能给用户设置角色。
  • 许可(Pemission) 对特定资源的特定的访问许可。

rbac0.png

3.2 RBAC1

RBAC1 在 RBAC0 的基础之上引入了角色继承的概念,有了继承那么角色就有了上下级或者等级关系。父角色拥有其子角色所有的许可。通俗讲就是来说: 你能干的,你的领导一定能干,反过来就不一定能行。

rbac1.png

3.3 RBAC2

在体育比赛中,你不可能既是运动员又是裁判员!

这是很有名的一句话。反应了我们经常出现的一种职务(其实也就是角色)冲突。有些角色产生的历史原因就是为了制约另一个角色,裁判员就是为了制约运动员从而让运动员按照规范去比赛。如果一个人兼任这两个角色,比赛必然容易出现不公正的情况从而违背竞技公平性准则。还有就是我们每个人在不同的场景都会充当不同的角色,在公司你就是特定岗位的员工,在家庭中你就是一名家庭成员。随着场景的切换,我们的角色也在随之变化。所以 RBAC2 在 RBAC0 的基础上引入了静态职责分离(Static Separation of Duty,简称SSD)和动态职责分离(Dynamic Separation of Duty,简称DSD)两个约束概念。他们两个作用的生命周期是不同的;

  • SSD 作用于约束用户和角色绑定时。 1.互斥角色:就像上面的例子你不能既是A又是B,互斥的角色只能二选一 ; 2. 数量约束:用户的角色数量是有限的不能多于某个基数; 3. 条件约束:只能达到某个条件才能拥有某个角色。经常用于用户等级体系,只有你充钱成为VIP才能一刀999。
  • DSD 作用于会话和角色交互时。当用户持有多个角色,在用户通过会话激活角色时加以条件约束,根据不同的条件执行不同的策略。

图就不画了就是在 RBAC0 加了上述两个约束。

3.4 RBAC3

我全都要!

RBAC1 和 RBAC2 各有神通。当你拿着这两个方案给产品经理看时,他给了你一个坚定的眼神:我全都要! 于是 RBAC3 就出现了。也就是说 RBAC3 = RBAC1 + RBAC2 。

  1. RBAC 中一些概念的理解

四个模型说完,我们来简单对其中的一些概念进行进一步的了解。

4.1 用户(User)

对用户的理解不应该被局限于单个用户,也可以是用户组(类似 linux 的 User Group), 或许还有其它的名字比如部门或者公司;也可以是虚拟的账户,客户,甚至说第三方应用也可以算用户 。所以对用户的理解要宽泛一些,只要是有访问资源需求的主体都可以纳入用户范畴。

4.2 角色(Role)

角色是特定许可的集合以及载体。一个角色可以包含多个用户,一个用户同样的也可以属于多个角色;同样的一个角色可以包含多个用户组,一个用户组也可以具有多个角色,所以角色和用户是多对多的关系。角色是可以细分的,也就是可以继承、可以分组的。

4.3 许可(Permission)

许可一般称它为权限。通常我们将访问的目标统称为资源,不管是数据还是静态资源都是资源。我们访问资源基本上又通过 api 接口来访问。所以一般权限都体现在对接口的控制上。再细分的话我将其划分为菜单控制,具体数据增删改查功能控制(前台体现为按钮)。另外许可具有原子性,不可再分。我们将许可授予角色时就是粒度最小的单元。

  1. 总结

基于角色的访问控制(RBAC)已成为高级访问控制的主要方法之一。通过RBAC,您可以控制最终用户在广义和精细级别上可以做什么。您可以指定用户是管理员,专家用户还是最终用户,并使角色和访问权限与组织中员工的职位保持一致。仅根据需要为员工完成工作的足够访问权限来分配权限。通过上面的介绍相信一定会让你有所收获。对我接下来的 Spring Security 实战干货 集成 RBAC 也是提前预一下热。其实不管你使用什么安全框架, RBAC 都是必须掌握的。如果你有什么问题可以留言讨论。也可以通过关注公众号 Felordcn 与我联系。

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

架构设计思路

发表于 2019-11-10

file

前言

我们一般在做架构设计的时候,会经历过三个阶段:需求分析、概要设计和详细设计。

  1. 需求分析阶段: 主要梳理所有用例(Use case)和场景,并抽象出面向系统的用户与角色,梳理出需求提供哪些功能与非功能的需求给这些用户。
  2. 概要设计阶段:根据需求分析的产物:核心需求,对整个系统进行模块划分,并定义好模块之间的交互关系。
  3. 详细设计阶段:通过多个视图来描述系统的架构,包括但不局限于:逻辑系统、物理视图、数据视图、物理视图

非功能需求

非功能的需求主要体现在高性能、高可用、可伸缩、可扩展、安全性等维度。

非功能需求对应不同系统指标

非功能需求对应不同系统指标主要分为 4 部分:

  • 应用服务器
  • 数据库
  • 缓存
  • 消息队列

1. 应用服务器

应用服务器是请求的入口,所有流量都是通过应用服务器来转发的。主要关心 QPS 、RT 等指标。容量与性能相关指标如下所示

1
2
3
4
5
6
7
8
复制代码1. 每天的请求量
2. 各接口的访问峰值
3. 平均响应时间
4. 最大响应时间
5. 请求大小
6. 网卡与磁盘 I/O 负责
7. 内存使用情况
8. CPU 使用情况

2. 数据库

部署结构相关指标

1
2
3
4
5
6
复制代码1. 复制模型
2. 失效转移策略
3. 容灾策略
4. 归档策略
5. 读写分离策略
6. 分库分表策略

容量与性能相关指标如下所示

1
2
3
4
5
复制代码1. 当前数据容量
2. 预估数据容量
3. 每秒读峰值
4. 每秒写峰值
5. 每秒事务峰值

3. 缓存

部署结构相关指标

1
2
3
4
5
复制代码1. 复制模型
2. 失效转移
3. 持久策略
4. 淘汰策略
5. 线程模型

容量与性能相关指标

1
2
3
4
5
6
复制代码1. 缓存内容大小
2. 缓存内容数量
3. 缓存内容过期时间
4. 缓存数据结构
5. 每秒读峰值
6. 每秒写峰值

4. 消息队列

部署结构相关指标

1
2
3
复制代码1. 复制模型
2. 失效转移
3. 持久策略

容量与性能相关指标

1
2
3
4
5
6
7
复制代码1. 每天平均数据增量
2. 消息保存时间
3. 每秒读峰值
4. 每秒写峰值
5. 每条消息大小
6. 平均响应时间
7. 最大响应时间

参考

  • 分布式服务架构原理、设计与实战

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1篇文章搞清楚8种JVM内存溢出(OOM)的原因和解决方法

发表于 2019-11-09

前言

撸Java的同学,多多少少会碰到内存溢出(OOM)的场景,但造成OOM的原因却是多种多样。堆溢出

这种场景最为常见,报错信息:

1
复制代码java.lang.OutOfMemoryError: Java heap space

原因

1、代码中可能存在大对象分配 2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。### 解决方法

1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性永久代/元空间溢出


报错信息:

1
复制代码java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace

原因

永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:* 字符串常量由永久代转移到堆中

  • 和永久代相关的JVM参数已移除

可能原因有如下几种:1、在Java7之前,频繁的错误使用String.intern()方法 2、运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载 3、应用长时间运行,没有重启没有重启 JVM 进程一般发生在调试时,如下面 tomcat 官网的一个 FAQ:

Why does the memory usage increase when I redeploy a web application? That is because your web application has a memory leak. A common issue are “PermGen” memory leaks. They happen because the Classloader (and the Class objects it loaded) cannot be recycled unless some requirements are met (). They are stored in the permanent heap generation by the JVM, and when you redeploy a new class loader is created, which loads another copy of all these classes. This can cause OufOfMemoryErrors eventually. (*) The requirement is that all classes loaded by this classloader should be able to be gc’ed at the same time.

解决方法

因为该OOM原因比较简单,解决方法有如下几种:1、检查是否永久代空间或者元空间设置的过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVMGC overhead limit exceeded


这个异常比较的罕见,报错信息:

1
复制代码java.lang.OutOfMemoryError:GC overhead limit exceeded

原因

这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。### 解决方法

1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。2、添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。3、dump内存,检查是否存在内存泄露,如果没有,加大内存。方法栈溢出

报错信息:

1
复制代码java.lang.OutOfMemoryError : unable to create new native Thread

原因

出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。### 解决方法

1、通过 -Xss 降低的每个线程栈大小的容量 2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:* /proc/sys/kernel/pid_max

  • /proc/sys/kernel/thread-max
  • maxuserprocess(ulimit -u)
  • /proc/sys/vm/maxmapcount

非常规溢出

下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下分配超大数组报错信息 :

1
复制代码java.lang.OutOfMemoryError: Requested array size exceeds VM limit

这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。解决方法就是检查你的代码中是否有创建超大数组的地方。swap溢出

报错信息 :

1
复制代码java.lang.OutOfMemoryError: Out of swap space

这种情况一般是操作系统导致的,可能的原因有:1、swap 分区大小分配不足;2、其他进程消耗了所有的内存。解决方案:1、其它服务进程可以选择性的拆分出去 2、加大swap分区大小,或者加大机器内存大小本地方法溢出

报错信息 :

1
复制代码java.lang.OutOfMemoryError: stack_trace_with_native_method

本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。这个异常出现的概率极低,只能通过操作系统本地工具进行诊断,难度有点大,还是放弃为妙。最后

欢迎大家关注我的公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

死磕 java线程系列之ForkJoinPool深入解析

发表于 2019-11-09

forkjoinpool

(手机横屏看源码更方便)

注:java源码分析部分如无特殊说明均基于 java8 版本。

注:本文基于ForkJoinPool分治线程池类。

简介

随着在硬件上多核处理器的发展和广泛使用,并发编程成为程序员必须掌握的一门技术,在面试中也经常考查面试者并发相关的知识。

今天,我们就来看一道面试题:

如何充分利用多核CPU,计算很大数组中所有整数的和?

剖析

  • 单线程相加?

我们最容易想到就是单线程相加,一个for循环搞定。

  • 线程池相加?

如果进一步优化,我们会自然而然地想到使用线程池来分段相加,最后再把每个段的结果相加。

  • 其它?

Yes,就是我们今天的主角——ForkJoinPool,但是它要怎么实现呢?似乎没怎么用过哈^^

三种实现

OK,剖析完了,我们直接来看三种实现,不墨迹,直接上菜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
复制代码/**
* 计算1亿个整数的和
*/
public class ForkJoinPoolTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 构造数据
int length = 100000000;
long[] arr = new long[length];
for (int i = 0; i < length; i++) {
arr[i] = ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
}
// 单线程
singleThreadSum(arr);
// ThreadPoolExecutor线程池
multiThreadSum(arr);
// ForkJoinPool线程池
forkJoinSum(arr);

}

private static void singleThreadSum(long[] arr) {
long start = System.currentTimeMillis();

long sum = 0;
for (int i = 0; i < arr.length; i++) {
// 模拟耗时,本文由公从号“彤哥读源码”原创
sum += (arr[i]/3*3/3*3/3*3/3*3/3*3);
}

System.out.println("sum: " + sum);
System.out.println("single thread elapse: " + (System.currentTimeMillis() - start));

}

private static void multiThreadSum(long[] arr) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();

int count = 8;
ExecutorService threadPool = Executors.newFixedThreadPool(count);
List<Future<Long>> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
int num = i;
// 分段提交任务
Future<Long> future = threadPool.submit(() -> {
long sum = 0;
for (int j = arr.length / count * num; j < (arr.length / count * (num + 1)); j++) {
try {
// 模拟耗时
sum += (arr[j]/3*3/3*3/3*3/3*3/3*3);
} catch (Exception e) {
e.printStackTrace();
}
}
return sum;
});
list.add(future);
}

// 每个段结果相加
long sum = 0;
for (Future<Long> future : list) {
sum += future.get();
}

System.out.println("sum: " + sum);
System.out.println("multi thread elapse: " + (System.currentTimeMillis() - start));
}

private static void forkJoinSum(long[] arr) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();

ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
// 提交任务
ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(new SumTask(arr, 0, arr.length));
// 获取结果
Long sum = forkJoinTask.get();

forkJoinPool.shutdown();

System.out.println("sum: " + sum);
System.out.println("fork join elapse: " + (System.currentTimeMillis() - start));
}

private static class SumTask extends RecursiveTask<Long> {
private long[] arr;
private int from;
private int to;

public SumTask(long[] arr, int from, int to) {
this.arr = arr;
this.from = from;
this.to = to;
}

@Override
protected Long compute() {
// 小于1000的时候直接相加,可灵活调整
if (to - from <= 1000) {
long sum = 0;
for (int i = from; i < to; i++) {
// 模拟耗时
sum += (arr[i]/3*3/3*3/3*3/3*3/3*3);
}
return sum;
}

// 分成两段任务,本文由公从号“彤哥读源码”原创
int middle = (from + to) / 2;
SumTask left = new SumTask(arr, from, middle);
SumTask right = new SumTask(arr, middle, to);

// 提交左边的任务
left.fork();
// 右边的任务直接利用当前线程计算,节约开销
Long rightResult = right.compute();
// 等待左边计算完毕
Long leftResult = left.join();
// 返回结果
return leftResult + rightResult;
}
}
}

彤哥偷偷地告诉你,实际上计算1亿个整数相加,单线程是最快的,我的电脑大概是100ms左右,使用线程池反而会变慢。

所以,为了演示ForkJoinPool的牛逼之处,我把每个数都/3*3/3*3/3*3/3*3/3*3了一顿操作,用来模拟计算耗时。

来看结果:

1
2
3
4
5
6
复制代码sum: 107352457433800662
single thread elapse: 789
sum: 107352457433800662
multi thread elapse: 228
sum: 107352457433800662
fork join elapse: 189

可以看到,ForkJoinPool相对普通线程池还是有很大提升的。

问题:普通线程池能否实现ForkJoinPool这种计算方式呢,即大任务拆中任务,中任务拆小任务,最后再汇总?

forkjoinpool

你可以试试看(-᷅_-᷄)

OK,下面我们正式进入ForkJoinPool的解析。

分治法

  • 基本思想

把一个规模大的问题划分为规模较小的子问题,然后分而治之,最后合并子问题的解得到原问题的解。

  • 步骤

(1)分割原问题:

(2)求解子问题:

(3)合并子问题的解为原问题的解。

在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。

  • 典型应用场景

(1)二分搜索

(2)大整数乘法

(3)Strassen矩阵乘法

(4)棋盘覆盖

(5)归并排序

(6)快速排序

(7)线性时间选择

(8)汉诺塔

ForkJoinPool继承体系

ForkJoinPool是 java 7 中新增的线程池类,它的继承体系如下:

forkjoinpool

ForkJoinPool和ThreadPoolExecutor都是继承自AbstractExecutorService抽象类,所以它和ThreadPoolExecutor的使用几乎没有多少区别,除了任务变成了ForkJoinTask以外。

这里又运用到了一种很重要的设计原则——开闭原则——对修改关闭,对扩展开放。

可见整个线程池体系一开始的接口设计就很好,新增一个线程池类,不会对原有的代码造成干扰,还能利用原有的特性。

ForkJoinTask

两个主要方法

  • fork()

fork()方法类似于线程的Thread.start()方法,但是它不是真的启动一个线程,而是将任务放入到工作队列中。

  • join()

join()方法类似于线程的Thread.join()方法,但是它不是简单地阻塞线程,而是利用工作线程运行其它任务。当一个工作线程中调用了join()方法,它将处理其它任务,直到注意到目标子任务已经完成了。

三个子类

  • RecursiveAction

无返回值任务。

  • RecursiveTask

有返回值任务。

  • CountedCompleter

无返回值任务,完成任务后可以触发回调。

ForkJoinPool内部原理

ForkJoinPool内部使用的是“工作窃取”算法实现的。

forkjoinpool

(1)每个工作线程都有自己的工作队列WorkQueue;

(2)这是一个双端队列,它是线程私有的;

(3)ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务;

(4)为了最大化地利用CPU,空闲的线程将从其它线程的队列中“窃取”任务来执行;

(5)从工作队列的尾部窃取任务,以减少竞争;

(6)双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的;

(7)当只剩下最后一个任务时,还是会存在竞争,是通过CAS来实现的;

forkjoinpool

ForkJoinPool最佳实践

(1)最适合的是计算密集型任务,本文由公从号“彤哥读源码”原创;

(2)在需要阻塞工作线程时,可以使用ManagedBlocker;

(3)不应该在RecursiveTask的内部使用ForkJoinPool.invoke()/invokeAll();

总结

(1)ForkJoinPool特别适合于“分而治之”算法的实现;

(2)ForkJoinPool和ThreadPoolExecutor是互补的,不是谁替代谁的关系,二者适用的场景不同;

(3)ForkJoinTask有两个核心方法——fork()和join(),有三个重要子类——RecursiveAction、RecursiveTask和CountedCompleter;

(4)ForkjoinPool内部基于“工作窃取”算法实现;

(5)每个线程有自己的工作队列,它是一个双端队列,自己从队列头存取任务,其它线程从尾部窃取任务;

(6)ForkJoinPool最适合于计算密集型任务,但也可以使用ManagedBlocker以便用于阻塞型任务;

(7)RecursiveTask内部可以少调用一次fork(),利用当前线程处理,这是一种技巧;

彩蛋

ManagedBlocker怎么使用?

答:ManagedBlocker相当于明确告诉ForkJoinPool框架要阻塞了,ForkJoinPool就会启另一个线程来运行任务,以最大化地利用CPU。

请看下面的例子,自己琢磨哈^^。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
复制代码/**
* 斐波那契数列
* 一个数是它前面两个数之和
* 1,1,2,3,5,8,13,21
*/
public class Fibonacci {

public static void main(String[] args) {
long time = System.currentTimeMillis();
Fibonacci fib = new Fibonacci();
int result = fib.f(1_000).bitCount();
time = System.currentTimeMillis() - time;
System.out.println("result,本文由公从号“彤哥读源码”原创 = " + result);
System.out.println("test1_000() time = " + time);
}

public BigInteger f(int n) {
Map<Integer, BigInteger> cache = new ConcurrentHashMap<>();
cache.put(0, BigInteger.ZERO);
cache.put(1, BigInteger.ONE);
return f(n, cache);
}

private final BigInteger RESERVED = BigInteger.valueOf(-1000);

public BigInteger f(int n, Map<Integer, BigInteger> cache) {
BigInteger result = cache.putIfAbsent(n, RESERVED);
if (result == null) {

int half = (n + 1) / 2;

RecursiveTask<BigInteger> f0_task = new RecursiveTask<BigInteger>() {
@Override
protected BigInteger compute() {
return f(half - 1, cache);
}
};
f0_task.fork();

BigInteger f1 = f(half, cache);
BigInteger f0 = f0_task.join();

long time = n > 10_000 ? System.currentTimeMillis() : 0;
try {

if (n % 2 == 1) {
result = f0.multiply(f0).add(f1.multiply(f1));
} else {
result = f0.shiftLeft(1).add(f1).multiply(f1);
}
synchronized (RESERVED) {
cache.put(n, result);
RESERVED.notifyAll();
}
} finally {
time = n > 10_000 ? System.currentTimeMillis() - time : 0;
if (time > 50)
System.out.printf("f(%d) took %d%n", n, time);
}
} else if (result == RESERVED) {
try {
ReservedFibonacciBlocker blocker = new ReservedFibonacciBlocker(n, cache);
ForkJoinPool.managedBlock(blocker);
result = blocker.result;
} catch (InterruptedException e) {
throw new CancellationException("interrupted");
}

}
return result;
// return f(n - 1).add(f(n - 2));
}

private class ReservedFibonacciBlocker implements ForkJoinPool.ManagedBlocker {
private BigInteger result;
private final int n;
private final Map<Integer, BigInteger> cache;

public ReservedFibonacciBlocker(int n, Map<Integer, BigInteger> cache) {
this.n = n;
this.cache = cache;
}

@Override
public boolean block() throws InterruptedException {
synchronized (RESERVED) {
while (!isReleasable()) {
RESERVED.wait();
}
}
return true;
}

@Override
public boolean isReleasable() {
return (result = cache.get(n)) != RESERVED;
}
}
}

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

qrcode

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Dubbo源码解析(二)Dubbo扩展机制SPI Dubbo

发表于 2019-11-07

Dubbo扩展机制SPI

前一篇文章《dubbo源码解析(一)Hello,Dubbo》是对dubbo整个项目大体的介绍,而从这篇文章开始,我将会从源码来解读dubbo再各个模块的实现原理以及特点,由于全部由截图的方式去解读源码会导致文章很杂乱,所以我只会放部分截图,全部的解读会同步更新在我github上fork的dubbo源码中,同时我也会在文章一些关键的地方加上超链接,方便读者快速查阅。

我会在之后的每篇文章前都写一个目标,为了让读者一眼就能知道本文是否是你需要寻找的资料。


目标:让读者知道JDK的SPI思想,dubbo的SPI思想,dubbo扩展机制SPI的原理,能够读懂实现扩展机制的源码。

第一篇源码分析的文章就先来讲讲dubbo扩展机制spi的原理,浏览过dubbo官方文档的朋友肯定知道,dubbo有大量的spi扩展实现,包括协议扩展、调用拦截扩展、路由扩展等26个扩展,并且spi机制运用到了各个模块设计中。所以我打算先讲解dubbo的扩展机制spi。

JDK的SPI思想

SPI的全名为Service Provider Interface,面向对象的设计里面,模块之间推荐基于接口编程,而不是对实现类进行硬编码,这样做也是为了模块设计的可拔插原则。为了在模块装配的时候不在程序里指明是哪个实现,就需要一种服务发现的机制,jdk的spi就是为某个接口寻找服务实现。jdk提供了服务实现查找的工具类:java.util.ServiceLoader,它会去加载META-INF/service/目录下的配置文件。具体的内部实现逻辑为这里先不展开,主要还是讲解dubbo关于spi的实现原理。

Dubbo的SPI扩展机制原理

dubbo自己实现了一套SPI机制,改进了JDK标准的SPI机制:

  1. JDK标准的SPI只能通过遍历来查找扩展点和实例化,有可能导致一次性加载所有的扩展点,如果不是所有的扩展点都被用到,就会导致资源的浪费。dubbo每个扩展点都有多种实现,例如com.alibaba.dubbo.rpc.Protocol接口有InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol等实现,如果只是用到其中一个实现,可是加载了全部的实现,会导致资源的浪费。
  2. 把配置文件中扩展实现的格式修改,例如META-INF/dubbo/com.xxx.Protocol里的com.foo.XxxProtocol格式改为了xxx = com.foo.XxxProtocol这种以键值对的形式,这样做的目的是为了让我们更容易的定位到问题,比如由于第三方库不存在,无法初始化,导致无法加载扩展名(“A”),当用户配置使用A时,dubbo就会报无法加载扩展名的错误,而不是报哪些扩展名的实现加载失败以及错误原因,这是因为原来的配置格式没有把扩展名的id记录,导致dubbo无法抛出较为精准的异常,这会加大排查问题的难度。所以改成key-value的形式来进行配置。
  3. dubbo的SPI机制增加了对IOC、AOP的支持,一个扩展点可以直接通过setter注入到其他扩展点。

我们先来看看SPI扩展机制实现的结构目录:

extension目录

(一)注解@SPI

在某个接口上加上@SPI注解后,表明该接口为可扩展接口。我用协议扩展接口Protocol来举例子,如果使用者在<dubbo:protocol />、<dubbo:service />、<dubbo:reference />都没有指定protocol属性的话,那么就会默认DubboProtocol就是接口Protocol,因为在Protocol上有@SPI(“dubbo”)注解。而这个protocol属性值或者默认值会被当作该接口的实现类中的一个key,dubbo会去META-INF\dubbo\internal\com.alibaba.dubbo.rpc.Protocol文件中找该key对应的value,看下图:

protocol的配置文件

value就是该Protocol接口的实现类DubboProtocol,这样就做到了SPI扩展。

(二)注解@Adaptive

该注解为了保证dubbo在内部调用具体实现的时候不是硬编码来指定引用哪个实现,也就是为了适配一个接口的多种实现,这样做符合模块接口设计的可插拔原则,也增加了整个框架的灵活性,该注解也实现了扩展点自动装配的特性。

dubbo提供了两种方式来实现接口的适配器:

  1. 在实现类上面加上@Adaptive注解,表明该实现类是该接口的适配器。

举个例子dubbo中的ExtensionFactory接口就有一个实现类AdaptiveExtensionFactory,加了@Adaptive注解,AdaptiveExtensionFactory就不提供具体业务支持,用来适配ExtensionFactory的SpiExtensionFactory和SpringExtensionFactory这两种实现。AdaptiveExtensionFactory会根据在运行时的一些状态来选择具体调用ExtensionFactory的哪个实现,具体的选择可以看下文Adaptive的代码解析。
2. 在接口方法上加@Adaptive注解,dubbo会动态生成适配器类。

我们从Transporter接口的源码来解释这种方法:

Transporter源码

我们可以看到在这个接口的bind和connect方法上都有@Adaptive注解,有该注解的方法的参数必须包含URL,ExtensionLoader会通过createAdaptiveExtensionClassCode方法动态生成一个Transporter$Adaptive类,生成的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码package com.alibaba.dubbo.remoting;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Transporter$Adaptive implements com.alibaba.dubbo.remoting.Transporter{

public com.alibaba.dubbo.remoting.Client connect(com.alibaba.dubbo.common.URL arg0, com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.remoting.RemotingException {
//URL参数为空则抛出异常。
if (arg0 == null)
throw new IllegalArgumentException("url == null");

com.alibaba.dubbo.common.URL url = arg0;
//这里的getParameter方法可以在源码中具体查看
String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
if(extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([client, transporter])");
//这里我在后面会有详细介绍
com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader

(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);
return extension.connect(arg0, arg1);
}
public com.alibaba.dubbo.remoting.Server bind(com.alibaba.dubbo.common.URL arg0, com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.remoting.RemotingException {
if (arg0 == null)
throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("server", url.getParameter("transporter", "netty"));
if(extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([server, transporter])");
com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader
(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);

return extension.bind(arg0, arg1);
}
}

可以看到该类的两个方法就是Transporter接口中有注解的两个方法,我来解释一下第一个方法connect:

1. 所有扩展点都通过传递URL携带配置信息,所以适配器中的方法必须携带URL参数,才能根据URL中的配置来选择对应的扩展实现。
2. @Adaptive注解中有一些key值,比如connect方法的注解中有两个key,分别为“client”和“transporter”,URL会首先去取client对应的value来作为我上述\*\*(一)注解@SPI\*\*中写到的key值,如果为空,则去取transporter对应的value,如果还是为空,则会根据SPI默认的key,也就是netty去调用扩展的实现类,如果@SPI没有设定默认值,则会抛出IllegalStateException异常。这样就比较清楚这个适配器如何去选择哪个实现类作为本次需要调用的类,这里最关键的还是强调了dubbo以URL为总线,运行过程中所有的状态数据信息都可以通过URL来获取,比如当前系统采用什么序列化,采用什么通信,采用什么负载均衡等信息,都是通过URL的参数来呈现的,所以在框架运行过程中,运行到某个阶段需要相应的数据,都可以通过对应的Key从URL的参数列表中获取。

(三)注解@Activate

扩展点自动激活加载的注解,就是用条件来控制该扩展点实现是否被自动激活加载,在扩展实现类上面使用,实现了扩展点自动激活的特性,它可以设置两个参数,分别是group和value。具体的介绍可以参照官方文档。

扩展点自动激活地址:dubbo.apache.org/zh-cn/docs/…

(四)接口ExtensionFactory

先来看看它的源码:

ExtensionFactory源码

该接口是扩展工厂接口类,它本身也是一个扩展接口,有SPI的注解。该工厂接口提供的就是获取实现类的实例,它也有两种扩展实现,分别是SpiExtensionFactory和SpringExtensionFactory代表着两种不同方式去获取实例。而具体选择哪种方式去获取实现类的实例,则在适配器AdaptiveExtensionFactory中制定了规则。具体规则看下面的源码解析。

(五)ExtensionLoader

该类是扩展加载器,这是dubbo实现SPI扩展机制等核心,几乎所有实现的逻辑都被封装在ExtensionLoader中。

详细代码注释见github:github.com/CrazyHZM/in…

  1. 属性(选取关键属性进行展开讲解,其余见github注释)
1. 关于存放配置文件的路径变量:



1
2
3
复制代码    private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
"META-INF/services/"、"META-INF/dubbo/"、"META-INF/dubbo/internal/"三个值,都是dubbo寻找扩展实现类的配置文件存放路径,也就是我在上述\*\*(一)注解@SPI\*\*中讲到的以接口全限定名命名的配置文件存放的路径。区别在于"META-INF/services/"是dubbo为了兼容jdk的SPI扩展机制思想而设存在的,"META-INF/dubbo/internal/"是dubbo内部提供的扩展的配置文件路径,而"META-INF/dubbo/"是为了给用户自定义的扩展实现配置文件存放。 2. 扩展加载器集合,key为扩展接口,例如Protocol等:
1
复制代码    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
3. 扩展实现类集合,key为扩展实现类,value为扩展对象,例如key为Class,value为DubboProtocol对象
1
复制代码    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
4. 以下属性都是cache开头的,都是出于性能和资源的优化,才做的缓存,读取扩展配置后,会先进行缓存,等到真正需要用到某个实现时,再对该实现类的对象进行初始化,然后对该对象也进行缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码    //以下提到的扩展名就是在配置文件中的key值,类似于“dubbo”等

//缓存的扩展名与拓展类映射,和cachedClasses的key和value对换。
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>();
//缓存的扩展实现类集合
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
//扩展名与加有@Activate的自动激活类的映射
private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>();
//缓存的扩展对象集合,key为扩展名,value为扩展对象
//例如Protocol扩展,key为dubbo,value为DubboProcotol
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object
//缓存的自适应( Adaptive )扩展对象,例如例如AdaptiveExtensionFactory类的对象
private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>();
//缓存的自适应扩展对象的类,例如AdaptiveExtensionFactory类
private volatile Class<?> cachedAdaptiveClass = null;
//缓存的默认扩展名,就是@SPI中设置的值
private String cachedDefaultName;
//创建cachedAdaptiveInstance异常
private volatile Throwable createAdaptiveInstanceError;
//拓展Wrapper实现类集合
private Set<Class<?>> cachedWrapperClasses;
//拓展名与加载对应拓展类发生的异常的映射
private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<String, IllegalStateException>();
这里提到了Wrapper类的概念。那我就解释一下:Wrapper类也实现了扩展接口,但是Wrapper类的用途是ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外,这实现了扩展点自动包装的特性。通俗点说,就是一个接口有很多的实现类,这些实现类会有一些公共的逻辑,如果在每个实现类写一遍这个公共逻辑,那么代码就会重复,所以增加了这个Wrapper类来包装,把公共逻辑写到Wrapper类中,有点类似AOP切面编程思想。这部分解释也可以结合官方文档: > 扩展点自动包装的特性地址:[dubbo.apache.org/zh-cn/docs/…](http://dubbo.apache.org/zh-cn/docs/dev/SPI.html)
  1. getExtensionLoader(Class type):根据扩展点接口来获得扩展加载器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
//扩展点接口为空,抛出异常
if (type == null)
throw new IllegalArgumentException("Extension type == null");
//判断type是否是一个接口类
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
//判断是否为可扩展的接口
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}

//从扩展加载器集合中取出扩展接口对应的扩展加载器
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);

//如果为空,则创建该扩展接口的扩展加载器,并且添加到EXTENSION_LOADERS
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}

这个方法的源码解析看上面,解读起来还是没有太多难点的。就是把几个属性的含义弄清楚就好了。
3. ##### getActivateExtension方法:获得符合自动激活条件的扩展实现类对象集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
复制代码	/**
* 获得符合自动激活条件的扩展实现类对象集合(适用没有group条件的自动激活类)
* @param url
* @param key
* @return
*/
public List<T> getActivateExtension(URL url, String key) {
return getActivateExtension(url, key, null);
}
//弃用
public List<T> getActivateExtension(URL url, String[] values) {
return getActivateExtension(url, values, null);
}
/**
* 获得符合自动激活条件的扩展实现类对象集合(适用含有value和group条件的自动激活类)
* @param url
* @param key
* @param group
* @return
*/
public List<T> getActivateExtension(URL url, String key, String group) {
String value = url.getParameter(key);
// 获得符合自动激活条件的拓展对象数组
return getActivateExtension(url, value == null || value.length() == 0 ? null : Constants.COMMA_SPLIT_PATTERN.split(value), group);
}

public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);

//判断不存在配置 `"-name"` 。
//例如,<dubbo:service filter="-default" /> ,代表移除所有默认过滤器。
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {

//获得扩展实现类数组,把扩展实现类放到cachedClasses中
getExtensionClasses();
for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Activate activate = entry.getValue();
//判断group值是否存在所有自动激活类中group组中,匹配分组
if (isMatchGroup(group, activate.group())) {
//通过扩展名获得拓展对象
T ext = getExtension(name);
//不包含在自定义配置里。如果包含,会在下面的代码处理。
//判断是否配置移除。例如 <dubbo:service filter="-monitor" />,则 MonitorFilter 会被移除
//判断是否激活
if (!names.contains(name)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activate, url)) {
exts.add(ext);
}
}
}
//排序
Collections.sort(exts, ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<T>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
//还是判断是否是被移除的配置
if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
//在配置中把自定义的配置放在自动激活的扩展对象前面,可以让自定义的配置先加载
//例如,<dubbo:service filter="demo,default,demo2" /> ,则 DemoFilter 就会放在默认的过滤器前面。
if (Constants.DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
T ext = getExtension(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}

可以看到getActivateExtension重载了四个方法,其实最终的实现都是在最后一个重载方法,因为自动激活类的条件可以分为无条件、只有value以及有group和value三种,具体的可以回顾上述**(三)注解@Activate**。

最后一个getActivateExtension方法有几个关键点:

1. group的值合法判断,因为group可选"provider"或"consumer"。
2. 判断该配置是否被移除。
3. 如果有自定义配置,并且需要放在自动激活扩展实现对象加载前,那么需要先存放自定义配置。
  1. getExtension方法: 获得通过扩展名获得扩展对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码    /**
* 通过扩展名获得扩展对象
* @param name
* @return
*/
@SuppressWarnings("unchecked")
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
//查找默认的扩展实现,也就是@SPI中的默认值作为key
if ("true".equals(name)) {
return getDefaultExtension();
}
//缓存中获取对应的扩展对象
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
//通过扩展名创建接口实现类的对象
instance = createExtension(name);
//把创建的扩展对象放入缓存
holder.set(instance);
}
}
}
return (T) instance;
}

这个方法中涉及到getDefaultExtension方法和createExtension方法,会在后面讲到。其他逻辑比较简单,就是从缓存中取,如果没有,就创建,然后放入缓存。
5. ##### getDefaultExtension方法:查找默认的扩展实现

1
2
3
4
5
6
7
8
9
10
复制代码    public T getDefaultExtension() {
//获得扩展接口的实现类数组
getExtensionClasses();
if (null == cachedDefaultName || cachedDefaultName.length() == 0
|| "true".equals(cachedDefaultName)) {
return null;
}
//又重新去调用了getExtension
return getExtension(cachedDefaultName);
}

这里涉及到getExtensionClasses方法,会在后面讲到。获得默认的扩展实现类对象就是通过缓存中默认的扩展名去获得实现类对象。
6. ##### addExtension方法:扩展接口的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码    public void addExtension(String name, Class<?> clazz) {
getExtensionClasses(); // load classes

//该类是否是接口的本身或子类
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Input type " +
clazz + "not implement Extension " + type);
}
//该类是否被激活
if (clazz.isInterface()) {
throw new IllegalStateException("Input type " +
clazz + "can not be interface!");
}

//判断是否为适配器
if (!clazz.isAnnotationPresent(Adaptive.class)) {
if (StringUtils.isBlank(name)) {
throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
}
if (cachedClasses.get().containsKey(name)) {
throw new IllegalStateException("Extension name " +
name + " already existed(Extension " + type + ")!");
}

//把扩展名和扩展接口的实现类放入缓存
cachedNames.put(clazz, name);
cachedClasses.get().put(name, clazz);
} else {
if (cachedAdaptiveClass != null) {
throw new IllegalStateException("Adaptive Extension already existed(Extension " + type + ")!");
}

cachedAdaptiveClass = clazz;
}
}
  1. getAdaptiveExtension方法:获得自适应扩展对象,也就是接口的适配器对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码   @SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//创建适配器对象
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}

return (T) instance;
}

思路就是先从缓存中取适配器类的对象,如果没有,则创建一个适配器对象,然后放入缓存,createAdaptiveExtension方法解释在后面给出。
8. ##### createExtension方法:通过扩展名创建扩展接口实现类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码    @SuppressWarnings("unchecked")
private T createExtension(String name) {
//获得扩展名对应的扩展实现类
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
//看缓存中是否有该类的对象
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
//向对象中注入依赖的属性(自动装配)
injectExtension(instance);
//创建 Wrapper 扩展对象(自动包装)
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}

这里运用到了两个扩展点的特性,分别是自动装配和自动包装。injectExtension方法解析在下面给出。
9. ##### injectExtension方法:向创建的拓展注入其依赖的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码    private T injectExtension(T instance) {
try {
if (objectFactory != null) {
//反射获得该类中所有的方法
for (Method method : instance.getClass().getMethods()) {
//如果是set方法
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
//获得属性,比如StubProxyFactoryWrapper类中有Protocol protocol属性,
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
//获得属性值,比如Protocol对象,也可能是Bean对象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
//注入依赖属性
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}

思路就是是先通过反射获得类中的所有方法,然后找到set方法,找到需要依赖注入的属性,然后把对象注入进去。
10. ##### getExtensionClass方法:获得扩展名对应的扩展实现类

1
2
3
4
5
6
7
8
9
10
复制代码    private Class<?> getExtensionClass(String name) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if (name == null)
throw new IllegalArgumentException("Extension name == null");
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null)
throw new IllegalStateException("No such extension \"" + name + "\" for " + type.getName() + "!");
return clazz;
}

这边就是调用了getExtensionClasses的方法,该方法解释在下面给出。
11. ##### getExtensionClasses方法:获得扩展实现类数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
//从配置文件中,加载扩展实现类数组
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}

这里思路就是先从缓存中取,如果缓存为空,则从配置文件中读取扩展实现类,loadExtensionClasses方法解析在下面给出。
12. loadExtensionClasses方法:从配置文件中,加载拓展实现类数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码   private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
//@SPI内的默认值
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
//只允许有一个默认值
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) cachedDefaultName = names[0];
}
}

//从配置文件中加载实现类数组
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}

前一部分逻辑是在把SPI注解中的默认值放到缓存中去,加载实现类数组的逻辑是在后面几行,关键的就是loadDirectory方法(解析在下面给出),并且这里可以看出去找配置文件访问的资源路径顺序。
13. ##### loadDirectory方法:从一个配置文件中,加载拓展实现类数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
//拼接接口全限定名,得到完整的文件名
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
//获取ExtensionLoader类信息
ClassLoader classLoader = findClassLoader();
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
//遍历文件
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}

这边的思路是先获得完整的文件名,遍历每一个文件,在loadResource方法中去加载每个文件的内容。
14. ##### loadResource方法:加载文件中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码   private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
while ((line = reader.readLine()) != null) {
//跳过被#注释的内容
final int ci = line.indexOf('#');
if (ci >= 0) line = line.substring(0, ci);
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
//根据"="拆分key跟value
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
//加载扩展类
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}

该类的主要的逻辑就是读取里面的内容,跳过“#”注释的内容,根据配置文件中的key=value的形式去分割,然后去加载value对应的类。
15. ##### loadClass方法:根据配置文件中的value加载扩展类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
复制代码   private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
//该类是否实现扩展接口
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
//判断该类是否为扩展接口的适配器
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {
//通过反射获得构造器对象
clazz.getConstructor();
//未配置扩展名,自动生成,例如DemoFilter为 demo,主要用于兼容java SPI的配置。
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
// 获得扩展名,可以是数组,有多个拓扩展名。
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
//如果是自动激活的实现类,则加入到缓存
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
//缓存扩展实现类
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}

重点关注该方法中兼容了jdk的SPI思想。因为jdk的SPI相关的配置文件中是xx.yyy.DemoFilter,并没有key,也就是没有扩展名的概念,所有为了兼容,通过xx.yyy.DemoFilter生成的扩展名为demo。
16. ##### createAdaptiveExtensionClass方法:创建适配器类,类似于dubbo动态生成的Transporter$Adpative这样的类

1
2
3
4
5
6
7
8
复制代码    private Class<?> createAdaptiveExtensionClass() {
//创建动态生成的适配器类代码
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//编译代码,返回该类
return compiler.compile(code, classLoader);
}

这个方法中就做了编译代码的逻辑,生成代码在createAdaptiveExtensionClassCode方法中,createAdaptiveExtensionClassCode方法由于过长,我不在这边列出,下面会给出github的网址,读者可自行查看相关的源码解析。createAdaptiveExtensionClassCode生成的代码逻辑可以对照我上述讲的**(二)注解@Adaptive**中的Transporter$Adpative类来看。
17. ##### 部分方法比较浅显易懂,并且没有影响主功能,所有我不在列举,该类的其他方法请在一下网址中查看,这里强调一点,其中的逻辑不难,难的是属性的含义要充分去品读理解,弄清楚各个属性的含义后,再看一些逻辑就很浅显易懂了。如果真的看不懂属性的含义,可以进入到调用的地方,结合“语境”去理解。

ExtensionLoader类源码解析地址:github.com/CrazyHZM/in…

(六)AdaptiveExtensionFactory

该类是ExtensionFactory的适配器类,也就是我在**(二)注解@Adaptive**中提到的第一种适配器类的使用。来看看该类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

//扩展对象的集合,默认的可以分为dubbo 的SPI中接口实现类对象或者Spring bean对象
private final List<ExtensionFactory> factories;

public AdaptiveExtensionFactory() {
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
//遍历所有支持的扩展名
for (String name : loader.getSupportedExtensions()) {
//扩展对象加入到集合中
list.add(loader.getExtension(name));
}
//返回一个不可修改的集合
factories = Collections.unmodifiableList(list);
}

@Override
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
//通过扩展接口和扩展名获得扩展对象
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}

}
  1. factories是扩展对象的集合,当用户没有自己实现ExtensionFactory接口,则这个属性就只会有两种对象,分别是 SpiExtensionFactory 和 SpringExtensionFactory 。
  2. 构造器中是把所有支持的扩展名的扩展对象加入到集合
  3. 实现了接口的getExtension方法,通过接口和扩展名来获取扩展对象。

(七)SpiExtensionFactory

SPI ExtensionFactory 拓展实现类,看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class SpiExtensionFactory implements ExtensionFactory {

@Override
public <T> T getExtension(Class<T> type, String name) {
//判断是否为接口,接口上是否有@SPI注解
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
//获得扩展加载器
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
if (!loader.getSupportedExtensions().isEmpty()) {
//返回适配器类的对象
return loader.getAdaptiveExtension();
}
}
return null;
}

}

(八)ActivateComparator

该类在ExtensionLoader类的getActivateExtension方法中被运用到,作为自动激活拓展对象的排序器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
复制代码public class ActivateComparator implements Comparator<Object> {

public static final Comparator<Object> COMPARATOR = new ActivateComparator();

@Override
public int compare(Object o1, Object o2) {
//基本排序
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
if (o1.equals(o2)) {
return 0;
}
Activate a1 = o1.getClass().getAnnotation(Activate.class);
Activate a2 = o2.getClass().getAnnotation(Activate.class);
//使用Activate注解的 `after` 和 `before` 属性,排序
if ((a1.before().length > 0 || a1.after().length > 0
|| a2.before().length > 0 || a2.after().length > 0)
&& o1.getClass().getInterfaces().length > 0
&& o1.getClass().getInterfaces()[0].isAnnotationPresent(SPI.class)) {
ExtensionLoader<?> extensionLoader = ExtensionLoader.getExtensionLoader(o1.getClass().getInterfaces()[0]);
if (a1.before().length > 0 || a1.after().length > 0) {
String n2 = extensionLoader.getExtensionName(o2.getClass());
for (String before : a1.before()) {
if (before.equals(n2)) {
return -1;
}
}
for (String after : a1.after()) {
if (after.equals(n2)) {
return 1;
}
}
}
if (a2.before().length > 0 || a2.after().length > 0) {
String n1 = extensionLoader.getExtensionName(o1.getClass());
for (String before : a2.before()) {
if (before.equals(n1)) {
return 1;
}
}
for (String after : a2.after()) {
if (after.equals(n1)) {
return -1;
}
}
}
}
// 使用Activate注解的 `order` 属性,排序。
int n1 = a1 == null ? 0 : a1.order();
int n2 = a2 == null ? 0 : a2.order();
// never return 0 even if n1 equals n2, otherwise, o1 and o2 will override each other in collection like HashSet
return n1 > n2 ? 1 : -1;
}

}

关键的还是通过@Activate注解中的值来进行排序。

后记

该部分相关的源码解析地址:github.com/CrazyHZM/in…

该文章讲解了dubbo的SPI扩展机制的实现原理,最关键的是弄清楚dubbo跟jdk在实现SPI的思想上做了哪些改进和优化,解读dubbo SPI扩展机制最关键的是弄清楚@SPI、@Adaptive、@Activate三个注解的含义,大部分逻辑都被封装在ExtensionLoader类中。dubbo的很多接口都是扩展接口,解读该文,也能让读者在后续文章中更加容易的去了解dubbo的架构设计。如果我在哪一部分写的不够到位或者写错了,欢迎给我提意见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《我们一起进大厂》系列-Redis哨兵、持久化、主从、手撕L

发表于 2019-11-07

你知道的越多,你不知道的越多

点赞再看,养成习惯

GitHub上已经开源github.com/JavaFamily,有一线大厂面试点脑图,欢迎Star和完善

絮叨

写这期其实比较纠结,我之前的写的比较通俗易懂,一是我都知道这些点,二是之前我在所在的电商公司对雪崩,击穿啥的还算有场景去接触。但是线上的Redis集群我实际操作经验很少,总不能在公司线上环境实践那些操作吧,所以最后看了下官网,还有一些资料(文章后面我都会贴出来),强行怼了这么篇出来。

最近双十一小忙,周末双十一值班目测没时间写,那我是暖男呀,我不能鸽啊,就有了这一篇,下一篇迟到你们不要喷我哈,而且下一篇还是Redis的终章还是得构思下,不熟悉的知识点我怕漏洞多,特意让以前的大牛同事看了下,所以有啥不对的地方大家及时留言Diss我,写这篇是真的难,诺下面就是我本人某天凌晨两点的拍的视频,多动症的仔。

(手机上动图可能太大加载失败,点进去这里可以看看这个图)

之前说过系列第二篇到300赞我就发第三篇

咋样没骗你们吧,就很枯竭,不BB了,开搞。

不点个赞对不起我,这次不要白嫖我!


正文

上几期《吊打面试官》还没看的小伙伴可以回顾一下(明明就写了两期说的好像很多一样)!

  • 《吊打面试官》系列-Redis基础
  • 《吊打面试官》系列-缓存雪崩、击穿、穿透

大家都知道一个技术的引入方便了开发,解决了各种问题,但是也会带来对应的问题,技术是把双刃剑嘛,集群的引入也会带来很多问题,如:集群的高可用怎么保证,数据怎么同步等等,我们话不多说,有请下一位受害者为我们展示。

面试开始

三个大腹便便,穿着格子衬衣的中年男子,拿着三个满是划痕的mac向你走来,看着快秃顶的头发,心想着肯定是尼玛顶级架构师吧!而且还是三个,但是还好我看过敖丙写的《吊打面试官》系列,腹有诗书气自华,根本虚都不虚好伐。

小伙子你好,之前问过了你基础知识以及一些缓存的常见几个大问题了,那你能跟我聊聊为啥Redis那么快么?

哦,帅气迷人的面试官您好,我们可以先看一下关系型数据库跟Redis本质上的区别。

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 使用多路I/O复用模型,非阻塞IO;
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

我可以问一下啥是上下文切换么?

我可以打个比方么:我记得有过一个小伙伴微信问过我上下文切换是啥,为啥可能会线程不安全,我是这么说的,就好比你看一本英文书,你看到第十页发现有个单词不会读,你加了个书签,然后去查字典,过了一会你又回来继续从书签那里读,ok到目前为止没啥问题。

如果是你一个人读肯定没啥问题,但是你去查的时候,别的小伙伴好奇你在看啥他就翻了一下你的书,然后溜了,哦豁,你再看的时候就发现书不是你看的那一页了。不知道到这里为止我有没有解释清楚,以及为啥会线程不安全,就是因为你一个人怎么看都没事,但是人多了换来换去的操作一本书数据就乱了。可能我的解释很粗糙,但是道理应该是一样的。

那他是单线程的,我们现在服务器都是多核的,那不是很浪费?

是的他是单线程的,但是,我们可以通过在单机开多个Redis实例嘛。

既然提到了单机会有瓶颈,那你们是怎么解决这个瓶颈的?

我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node。

这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。

哦?那问题就来了,他们之间是怎么进行数据交互的?以及Redis是怎么进行持久化的?Redis数据都在内存中,一断电或者重启不就木有了嘛?

是的,持久化的话是Redis高可用中比较重要的一个环节,因为Redis数据在内存的特性,持久化必须得有,我了解到的持久化是有两种方式的。

  • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。

两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF更适合做热备,比如我杭州的某电商公司有这两个数据,我备份一份到我杭州的节点,再备份一个到上海的,就算发生无法避免的自然灾害,也不会两个地方都一起挂吧,这灾备也就是异地容灾,地球毁灭他没办法。

tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

那这两种机制各自优缺点是啥?

我先说RDB吧

优点:

他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。

RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

缺点:

RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。

还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。

我们再来说说AOF

优点:

上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。

AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。

AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

tip:我说的命令你们别真去线上系统操作啊,想试去自己买的服务器上装个Redis试,别到时候来说,敖丙真是个渣男,害我把服务器搞崩了,Redis官网上的命令都去看看,不要乱试!!!

缺点:

一样的数据,AOF文件比RDB还要大。

AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。

那两者怎么选择?

小孩子才做选择,我全都要,你单独用RDB你会丢失很多数据,你单独用AOF,你数据恢复没RDB来的快,真出什么时候第一时间用RDB恢复,然后AOF做数据补全,真香!冷备热备一起上,才是互联网时代一个高健壮性系统的王道。

看不出来年纪轻轻有点东西的呀,对了我听你提到了高可用,Redis还有其他保证集群高可用的方式么?

!!!晕 自己给自己埋个坑(其实是明早就准备好了,故意抛出这个词等他问,就怕他不问)。

假装思考一会(不要太久,免得以为你真的不会),哦我想起来了,还有哨兵集群sentinel。

哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。

为啥必须要三个实例呢?我们先看看两个哨兵会咋样。

master宕机了 s1和s2两个哨兵只要有一个认为你宕机了就切换了,并且会选举出一个哨兵去执行故障,但是这个时候也需要大多数哨兵都是运行的。

那这样有啥问题呢?M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下S2个裸屌了,没有哨兵去允许故障转移了,虽然另外一个机器上还有R1,但是故障转移就是不执行。

经典的哨兵集群是这样的:

M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。

暖男我,小的总结下哨兵组件的主要功能:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

我记得你还提到了主从同步,能说一下主从之间的数据怎么同步的么?

面试官您的记性可真是一级棒呢,我都要忘了你还记得,我特么谢谢你,提到这个,就跟我前面提到的数据持久化的RDB和AOF有着比密切的关系了。

我先说下为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。

回归正题,他们数据怎么同步的呢?

你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。

数据传输的时候断网了或者服务器挂了怎么办啊?

传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的。

大家需要记得的就是,RDB快照的数据生成的时候,缓存区也必须同时开始接受新请求,不然你旧的数据过去了,你在同步期间的增量数据咋办?是吧?

那说了这么多你能说一下他的内存淘汰机制么,来手写一下LRU代码?

手写LRU?你是不是想直接跳起来说一句:Are U F**k Kidding me?

这个问题是我在蚂蚁金服三面的时候亲身被问过的问题,不知道大家有没有被怼到过这个问题。

Redis的过期策略,是有定期删除+惰性删除两种。

定期好理解,默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。

为啥不扫描全部设置了过期时间的key呢?

假如Redis里面所有的key都有过期时间,都扫描一遍?那太恐怖了,而且我们线上基本上也都是会设置一定的过期时间的。全扫描跟你去查数据库不带where条件不走索引全表扫描一样,100ms一次,Redis累都累死了。

如果一直没随机到很多key,里面不就存在大量的无效key了?

好问题,惰性删除,见名知意,惰性嘛,我不主动删,我懒,我等你来查询了我看看你过期没,过期就删了还不给你返回,没过期该怎么样就怎么样。

最后就是如果的如果,定期没删,我也没查询,那可咋整?

内存淘汰机制!

官网上给到的内存淘汰机制是以下几个:

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

至于LRU我也简单提一下,手写实在是太长了,大家可以去Redis官网看看,我把近视LUR效果给大家看看

tip:Redis为什么不使用真实的LRU实现是因为这需要太多的内存。不过近似的LRU算法对于应用而言应该是等价的。使用真实的LRU算法与近似的算法可以通过下面的图像对比。

LRU comparison

LRU comparison

你可以看到三种点在图片中, 形成了三种带.

  • 浅灰色带是已经被回收的对象。
  • 灰色带是没有被回收的对象。
  • 绿色带是被添加的对象。
  • 在LRU实现的理论中,我们希望的是,在旧键中的第一半将会过期。Redis的LRU算法则是概率的过期旧的键。

你可以看到,在都是五个采样的时候Redis 3.0比Redis 2.8要好,Redis2.8中在最后一次访问之间的大多数的对象依然保留着。使用10个采样大小的Redis 3.0的近似值已经非常接近理论的性能。

注意LRU只是个预测键将如何被访问的模型。另外,如果你的数据访问模式非常接近幂定律,大部分的访问将集中在一个键的集合中,LRU的近似算法将处理得很好。

其实在大家熟悉的LinkedHashMap中也实现了Lru算法的,实现如下:

当容量超过100时,开始执行LRU策略:将最近最少未使用的 TimeoutInfoHolder 对象 evict 掉。

真实面试中会让你写LUR算法,你可别搞原始的那个,那真TM多,写不完的,你要么怼上面这个,要么怼下面这个,找一个数据结构实现下Java版本的LRU还是比较容易的,知道啥原理就好了。

面试结束

小伙子,你确实有点东西,HRBP会联系你的,请务必保持你的手机畅通好么?

好的谢谢面试官,面试官真好,我还想再面几次,噗此。

能回答得这么全面这么细节还是忍不住点赞

(暗示点赞,每次都看了不点赞,你们想白嫖我么?你们好坏喲,不过我好喜欢)

总结

好了,我们玩归玩,闹归闹,别拿面试开玩笑,我这么写是为了节目效果,大家面试请认真对待。

这一期是这期没前面好理解了对吧,我就在自己的服务器上启动了,然后再去官网看看命令一顿瞎操作的,查阅了部分资料,这里给大家推荐几本经典的Redis入门的书籍和我参考的资料。

  • Redis中文官网
  • 《Redis入门指南(第2版)》
  • 《Redis实战》
  • 《Redis设计与实现》
  • 《大型网站技术架构——李智慧》
  • 《Redis 设计与实现——黄健宏》
  • 《Redis 深度历险——钱文品》
  • 《亿级流量网站架构核心技术——张开涛》
  • 《中华石杉——石杉》

不出意外的话这是Redis的倒数第二期,最后一期不知道写啥还没想好,我得好好想想,加上最近不是双十一嘛得加加班,你看看开头的我,多可怜,那还不点个赞?买个服务器?不确定下一期多久出,想早点看到更新的小伙伴可以去公众号催更,公众号提前一到两天更新。

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。

我后面会每周都更新几篇一线互联网大厂面试和常用技术栈相关的文章,非常感谢人才们能看到这里,如果这个文章写得还不错,觉得「敖丙」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!

白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

敖丙 | 文 【原创】

如果本篇博客有任何错误,请批评指教,不胜感激 !


文章每周持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/JavaFamily 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Dubbo 全链路追踪日志的实现

发表于 2019-11-07

微服务架构的项目,一次请求可能会调用多个微服务,这样就会产生多个微服务的请求日志,当我们想要查看整个请求链路的日志时,就会变得困难,所幸的是我们有一些集中日志收集工具,比如很热门的ELK,我们需要把这些日志串联起来,这是一个很关键的问题,如果没有串联起来,查询起来很是很困难,我们的做法是在开始请求系统时生成一个全局唯一的id,这个id伴随这整个请求的调用周期,即当一个服务调用另外一个服务的时候,会往下传递,形成一条链路,当我们查看日志时,只需要搜索这个id,整条链路的日志都可以查出来了。

现在以dubbo微服务架构为背景,举个栗子:

1
clean复制代码A -> B -> C

我们需要将A/B/C/三个微服务间的日志按照链式打印,我们都知道Dubbo的RpcContext只能做到消费者和提供者共享同一个RpcContext,比如A->B,那么A和B都可以获取相同内容的RpcContext,但是B->C时,A和C就无法共享相同内容的RpcContext了,也就是无法做到链式打印日志了。

那么我们是如何做到呢?

我们可以用左手交换右手的思路来解决,假设左手是线程的ThreadLocal,右手是RpcContext,那么在交换之前,我们首先将必要的日志信息保存到ThreadLocal中。

在我们的项目微服务中大致分为两种容器类型的微服务,一种是Dubbo容器,这种容器的特点是只使用spring容器启动,然后使用dubbo进行服务的暴露,然后将服务注册到zookeeper,提供服务给消费者;另一种是SpringMVC容器,也即是我们常见的WEB容器,它是我们项目唯一可以对外开放接口的容器,也是充当项目的网关功能。

在了解了微服务容器之后,我们现在知道了调用链的第一层一定是在SpringMVC容器层中,那么我们直接在这层写个自定义拦截器就ojbk了,talk is cheap,show you the demo code:

举例一个Demo代码,公共拦截器的前置拦截中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
reasonml复制代码public class CommonInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler)
throws Exception {

// ...

// 初始化全局的Context容器
Request request = initRequest(httpServletRequest);
// 新建一个全局唯一的请求traceId,并set进request中
request.setTraceId(JrnGenerator.genTraceId());
// 将初始化的请求信息放进ThreadLocal中
Context.initialLocal(request);

// ...

return true;
}

// ...

}

系统内部上下文对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
aspectj复制代码public class Context {

// ...

private static final ThreadLocal<Request> REQUEST_LOCAL = new ThreadLocal<>();

public final static void initialLocal(Request request) {
if (null == request) {
return;
}
REQUEST_LOCAL.set(request);
}

public static Request getCurrentRequest() {
return REQUEST_LOCAL.get();
}

// ...
}

拦截器实现了org.springframework.web.servlet.HandlerInterceptor接口,它的主要作用是用于拦截处理请求,可以在MVC层做一些日志记录与权限检查等操作,这相当于MVC层的AOP,即符合横切关注点的所有功能都可以放入拦截器实现。

这里的initRequest(httpServletRequest);就是将请求信息封装成系统内容的请求对象Request,并初始化一个全局唯一的traceId放进Request中,然后再把它放进系统内部上下文ThreadLocal字段中。

接下来讲讲如何将ThreadLocal中的内容放到RpcContext中,在讲之前,我先来说说Dubbo基于spi扩展机制,官方文档对拦截器扩展解释如下:

服务提供方和服务消费方调用过程拦截,Dubbo 本身的大多功能均基于此扩展点实现,每次远程方法执行,该拦截都会被执行,请注意对性能的影响。

也就是说我们进行服务远程调用前,拦截器会对此调用进行拦截处理,那么就好办了,在消费者调用远程服务之前,我们可以偷偷把ThreadLocal的内容放进RpcContext容器中,我们可以基于dubbo的spi机制扩展两个拦截器,一个在消费者端生效,另一个在提供者端生效:

在META-INF中加入com.alibaba.dubbo.rpc.Filter文件,内容如下:

1
2
stylus复制代码provider=com.objcoding.dubbo.filter.ProviderFilter
consumer=com.objcoding.dubbo.filter.ConsumerFilter

消费者端拦截处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
public class ConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation)
throws RpcException {

//1.从ThreadLocal获取请求信息
Request request = Context.getCurrentRequest();
//2.将Context参数放到RpcContext
RpcContext rpcCTX = RpcContext.getContext();
// 将初始化的请求信息放进ThreadLocal中
Context.initialLocal(request);

// ...

}
}

Context.getCurrentRequest();就是从ThreadLocal中拿到Request请求内容,contextToDubboContext(request);将Request内容放进当前线程的RpcContext容器中。

很容易联想到提供者也就是把RpcContext中的内容拿出来放到ThreadLocal中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala复制代码public class ProviderFilter extends AbstractDubboFilter implements Filter{
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation)
throws RpcException {
// 1.获取RPC远程调用上下文
RpcContext rpcCTX = RpcContext.getContext();
// 2.初始化请求信息
Request request = dubboContextToContext(rpcCTX);
// 3.将初始化的请求信息放进ThreadLocal中
Context.initialLocal(request);

// ...

}
}

接下来我们还要配置log4j2,使得我们同一条请求在关联的每一个容器打印的消息,都有一个共同的traceId,那么我们在ELK想要查询某个请求时,只需要搜索traceId,就可以看到整条请求链路的日志了。

我们在Context上下文对象的initialLocal(Request request)方法中在log4j2的上下文中添加traceId信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class Context {

// ...

final public static String TRACEID = "_traceid";

public final static void initialLocal(Request request) {
if (null == request) {
return;
}
// 在log4j2的上下文中添加traceId
ThreadContext.put(TRACEID, request.getTraceId());
REQUEST_LOCAL.set(request);
}

// ...
}

接下来实现org.apache.logging.log4j.core.appender.rewrite.RewritePolicy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
reasonml复制代码@Plugin(name = "Rewrite", category = "Core", elementType = "rewritePolicy", printObject = true)
public final class MyRewritePolicy implements RewritePolicy {

// ...

@Override
public LogEvent rewrite(final LogEvent source) {
HashMap<String, String> contextMap = Maps.newHashMap(source.getContextMap());
contextMap.put(Context.TRACEID, contextMap.containsKey(Context.TRACEID) ? contextMap.get(Context.TRACEID) : NULL);
return new Log4jLogEvent.Builder(source).setContextMap(contextMap).build();
}

// ...
}

RewritePolicy的作用是我们每次输出日志,log4j都会调用这个类进行一些处理的操作。

配置log4j2.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dust复制代码<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout
pattern="[%d{yyyy/MM/dd HH:mm:ss,SSS}][${ctx:_traceid}]%m%n" />
</Console>

<!--定义一个Rewrite-->
<Rewrite name="Rewrite">
<MyRewritePolicy/>
<!--引用输出模板-->
<AppenderRef ref="Console"/>
</Rewrite>
</Appenders>
<Loggers>

<!--使用日志模板-->
<Logger name="com.objcoding.MyLogger" level="debug" additivity="false">
<!--引用Rewrite-->
<AppenderRef ref="Rewrite"/>
</Logger>
</Loggers>
</Configuration>

自定义日志类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码public class MyLogger {
private static final Logger logger = LoggerFactory.getLogger(MyLogger.class);

public static void info(String msg, Object... args) {
if (canLog() == 1 && logger.isInfoEnabled()) {
logger.info(msg, args);
}
}

public static void debug(String message, Object... args) {
if (canLog() == 1 && logger.isDebugEnabled()) {
logger.debug(message, args);
}
}

// ..
}

更多精彩文章请关注作者维护的公众号「后端进阶」,这是一个专注后端相关技术的公众号。

关注公众号并回复「后端」免费领取后端相关电子书籍。

欢迎分享,转载请保留出处。

公众号「后端进阶」,专注后端技术分享!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

开源 CMS Ghost 30 发布,带来新功能

发表于 2019-11-05

Ghost 是一个自由开源的内容管理系统(CMS)。如果你还不了解 CMS,那我在此解释一下。CMS 是一种软件,用它可以构建主要专注于创建内容的网站,而无需了解 HTML 和其他与 Web 相关的技术。

事实上,Ghost 是目前最好的开源 CMS 之一。它主要聚焦于创建轻量级、快速加载、界面美观的博客。

Ghost 系统有一个现代直观的编辑器,该编辑器内置 SEO(搜索引擎优化)功能。你也可以用本地桌面(包括 Linux 系统)和移动应用程序。如果你喜欢终端,也可以使用其提供的 CLI(命令行界面)工具。

让我们看看 Ghost 3.0 带来了什么新功能。

Ghost 3.0 的新功能

我通常对开源的 CMS 解决方案很感兴趣。因此,在阅读了官方公告后,我通过在 Digital Ocean 云服务器上安装新的 Ghost 实例来进一步尝试它。

与以前的版本相比,Ghost 3.0 在功能和用户界面上的改进给我留下了深刻的印象。

在此,我将列出一些值得一提的关键点。

书签卡

除了编辑器的所有细微更改之外,3.0 版本现在支持通过输入 URL 添加漂亮的书签卡。

如果你使用过 WordPress(你可能已经注意到,WordPress 需要添加一个插件才能添加类似的卡片),所以该功能绝对是 Ghost 3.0 系统的一个最大改进。

改进的 WordPress 迁移插件

我没有专门对此进行测试,但它更新了 WordPress 的迁移插件,可以让你轻松地将帖子(带有图片)克隆到 Ghost CMS。

基本上,使用该插件,你就能够创建一个存档(包含图片)并将其导入到 Ghost CMS。

响应式图像库和图片

为了使用户体验更好,Ghost 团队还更新了图像库(现已为响应式),以便在所有设备上舒适地呈现你的图片集。

此外,帖子和页面中的图片也更改为响应式的了。

添加成员和订阅选项

Ghost Subscription Model

虽然,该功能目前还处于测试阶段,但如果你是以此平台作为维持你业务关系的重要发布平台,你可以为你的博客添加成员、订阅选项。

该功能可以确保只有订阅的成员才能访问你的博客,你也可以选择让未订阅者也可以访问。

Stripe:集成支付功能

默认情况下,该版本支持 Stripe 付款网关,帮助你轻松启用订阅功能(或使用任何类型的付款的付款方式),而 Ghost 不收取任何额外费用。

新的应用程序集成

你现在可以在 Ghost 3.0 的博客中集成各种流行的应用程序/服务。它可以使很多事情自动化。

默认主题改进

引入的默认主题(设计)已得到改进,现在也提供了夜间模式。

你也可以随时选择创建自定义主题(如果没有可用的预置主题)。

其他小改进

除了所有关键亮点以外,用于创建帖子/页面的可视编辑器也得到了改进(具有某些拖放功能)。

我确定还有很多技术方面的更改,如果你对此感兴趣,可以在他们的更改日志中查看。

Ghost 影响力渐增

要在以 WordPress 为主导的世界中获得认可并不是一件容易的事。但 Ghost 逐渐形成了它的一个专门的发布者社区。

不仅如此,它的托管服务 Ghost Pro 现在拥有像 NASA、Mozilla 和 DuckDuckGo 这样的客户。

在过去的六年中,Ghost 从其 Ghost Pro 客户那里获得了 500 万美元的收入。就从它是致力于开源系统解决方案的非营利组织这一点来讲,这确实是一项成就。

这些收入有助于它们保持独立,避免风险投资家的外部资金投入。Ghost CMS 的托管客户越多,投入到免费和开源的 CMS 的研发款项就越多。

总体而言,Ghost 3.0 是迄今为止提供的最好的升级版本。这些功能给我留下了深刻的印象。

如果你拥有自己的网站,你会使用什么 CMS?你曾经使用过 Ghost 吗?你的体验如何?请在评论部分分享你的想法。


via: itsfoss.com/ghost-3-rel…

作者:Ankush Das
选题:lujun9972
译者:Morisun029
校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…849850851…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%