库存扣减的那些事

库存扣减的那些事

常见问题

场景

  • 平时我们网购下单,存在一些商品下单后支付成功,还有一些场景可能最后没有支付成功

问题:系统何时减库存

  • 用户下单时扣减?
  • 用户支付完成时扣减?

常见扣减方式

下单减库存

  • 用户下单成功,扣减库存
  • 特征
    • 方式简单,控制最精确,通过数据库事务机制控制商品库存,不会出现超卖
  • 问题
    • 正常情况下,用户下单后一般会很大概率付款,不会有大问题。但是可能会有黄牛刷单的风险,可以做到无风险疯狂占用库存

支付减库存

  • 用户下单后,并不立即扣减,而是用户完成支付后,才真正扣减库存,库存一直留给真正付款的用户
  • 特征
    • 避免黄牛 or 恶意下单
  • 问题
    • 并发高的情况下,用户下单后可能会出现付不了款(超卖问题)
      • 超卖解决办法
        • 商家允许的情况下,通过补货来解决
        • 商家不允许的情况下,提示库存不足,不能完成支付

预扣库存(典型的预扣库存方案)

  • 用户下单后,库存保留一定的时间(比如预售:30分钟),超过时间未付款,库存自动释放,留给其他用户继续购买
  • 特征
    • 下单时,预减库存,检测库存是否有充足
      • 充足,扣减库存
      • 不足,下单失败,提醒用户
    • 支付完成时,校验订单有效性
      • 订单未过期,支付成功
        • 订单过期,其实库存已经释放,给用户退款
  • 问题
    • 虽然设置了库存的保留时间,但是用户可以在释放库存后再次钱柜,或者一次抢购大量库存抢占
    • 并不能彻底解决超卖问题,我们目前都是使用第三方支付,没办法在订单到期释放库存的时候一定完成了付款,在释放库存和支付之间的临界值,假如出现并发,很可能还是导致超卖,但是这种是非常极端的场景
  • 解决办法
    • 结合风控和反作弊的措施来制止,例如:控制购买数量、未付款的情况下不能再次下单等等

      大型秒杀场景

  • 一般秒杀情况下,商品都是爆款(可以理解为”抢到就是赚到”),下单后不付款情况很少,同时商家对于库存有严格的限制,”下单减库存”更加合理,同时该方式对比其他两种场景来说,逻辑上更加简单,性能上占有优势。
  • 如何保障一致性?
    • 根本目的就是保证数据库中库存字段对应的数值不能小于0!
  • 常见解决方案
    • 系统中通过事务来判断,保证扣减后数据不为负数,否则回滚
    • 设置数据库字段数据为无符号证书,通过扣减是,字段值小于零触发sql语句报错
    • update stock set 剩余库存 = 当前剩余库存-扣减库存 where 当前剩余库存-扣减库存 >= 0 实现比乐观锁更乐观的一种控制

先看一个库存扣减的设计

  • 简单描述下图中的设计,先扣减缓存,缓存扣减成功扣减db,若缓存扣减成功,db扣减失败,上锁同步,如果发现库存为0,将缓存设置为-999,防止缓存穿透,如果缓存扣件失败,也直接上锁同步db
  • 如果你觉得这个还不够复杂,等等,你是不是还忽略了一个问题,这个仅仅只是库存扣减,还没有涉及退款,超时等库存的回补操作,这个设计在分布式场景下,复杂度已经很高了

具体问题具体分析

  • 我们在库存扣减设计中核心要解决的问题是什么?
    • 库存扣减环节中,主要解决的问题是”并发写”
  • 写在哪里?
    • 缓存 or 数据库~
  • 缓存中扣减库存
    • 解决的是性能问题,复杂场景的关系映射不能很好支持,但是不支持事务
  • 数据库中扣减

    • 特性
      • 基于Mysql数据存储的特性,同一个sku的库存扣减肯定是在同一行,会有大量的线程竞争InnoDB行锁,并发越高,等待线程就越多,TPS会下降,系统的整体RT就会跟着上升,严重影响到数据库的吞吐量
    • 问题
      • 常见的电商场景中,单个热点商品会影响整个数据库的性能,导致0.01%的商品影响99.99%商品的售卖。
    • 解决办法

库存扣减的核心到底在哪里

  • 通过上面的分析我们可以得出一个结论
    • 在上面举出的复杂例子中,缓存起到的作用,其实跟偏向于做了一层限流,防止流量都压到数据库上
    • 库存扣减的瓶颈不在应用层面,而在于数据库层面

数据库能做哪些优化

  • I: 关闭死锁检测,提高并发处理性能
    对于秒杀/热卖商品这种情况,通过认真的分析,可以得出这种情况的特点,首先是很直接,其次是很暴力,正常的业务死锁会变成超时,最后是不治标,除掉老大,还有老二,问题的症结没有解决,这时候究竟该如何解决呢?经过不断努力,得出了第一种解决方法关掉死锁检测。

  • II:修改源代码,将排队提到进入引擎层前,降低引擎层面的并发度。
    如果请求一股脑的涌入数据库,势必会由于争抢资源造成性能下降,通过排队,让请求从混沌到有序,从而避免数据库在协调大量请求时过载。
    对于在固定的硬件条件下、每个系统都有一个对应的状态最优值,那么在InnoDB的线程数下,将排队队列提到进入引擎层前,这样就能够很好的解决在性能方面具有很好的提高。

  • III:组提交,降低server和引擎的交互次数,降低IO消耗,请求合并:甲买了一个商品,乙也买了同一个商品,与其把甲乙当做当做单独的请求分别执行一次商品库存减一的操作,不如把他们合并后统一执行一次商品库存减二的操作,请求合并的越多,效率提升的就越大。
    根据前面的排队技术,利用多线程并发下,InnoDB内部要做死锁检测等操作,会对性能影响及其严重,明确的串行事务,则server层串行,Group commit减少引擎执行次数,让性能最佳优。

  • IV:目前的一些云上数据库解决方案是一种不错的选择

写在最后

  • Redis只是解决性能问题,数据库才是解决库存一致性问题
  • 没有万能的系统,任何算法和架构都顶不住巨大的流量,限流是万精油!

记阿里面试经历

阿里面试经历

第一次面试

盒马一面

  • 三月中旬,接到盒马的电话面试邀请,约定在一周后的晚上7点进行第一轮电面,这次面试大概一个半小时,面试官先是问了项目,讲一讲最近的项目,然后如何设计的,在设计中遇到了什么,并且提出一些他对于这个设计的一些疑问,项目问完后,问的是技术,技术点主要是以下几点:
  • 面试官:平时项目中有用到哪些中间件,能不能介绍下.我的回答:Dubbo,RocketMQ,Redis等等,但是RocketMQ,Redis这些仅仅只是应用,没有看过源码,对于其只是简单了解,Dubbo我就比较熟,现在也活跃在社区为Dubbo做一些贡献
  • 面试官:如果让你自己实现RPC,你会怎么样去做?我的回答:注册中心(zk,etcd,nacos,redis),然后一个provider一个consumer,要实现网络通信,就需要netty/mina这种中间件来做网络通信,巴拉巴拉
  • 面试官:那讲一下Dubbo你最熟悉的模块吧.我的回答:从dubbo的架构到服务注册发现调用,以及dubbo的spi机制,集群容错策略等等
  • 面试官:对于NIO有了解过吗.我的回答:有,但是只是很简单的了解,没有深入阅读源码,这块还是不做展开吧(netty和NIO这块是我的弱项,问道这个真的很头大)
  • 面试官:java的类加载机制和内存模型能够讲一讲吗.这个问题大概就不用展开了
  • 面试官:介绍一下tcp/ip协议.我的回答:三次握手,四次挥手(这块由于当时紧张,就很简单的说了下,然后给面试官说突然有的反应不过来了,这个问题还是算了吧,面试官也就没有继续追问)
  • 面试官:spring和mybatis有了解过吗.我的回答:有了解,但是没有深入源码,只能大概聊一下,没有dubbo那么熟(面试官也没有技术追问)
  • 面试官:如何创建多线程,如何让多个线程在同一时间执行.这个问题也就不用展开了
  • 后来就是mysql的一些索引优化,mysql的基础架构等等问题,最后面试官问我,有没有什么问题,我就问了下如果有下一面大概是什么时候有通知,面试官说最近忙,正常的话大概是一周左右,我说好的,就这样一面结束

盒马二面

  • 周六在回家的公交车,二面面试官打来了电话,准备进行二面,当时在车上,我就说一个小时后后到家在聊,一个小时后回到家里,如约进行了二面,时间接近40分钟
  • 二面主要就是讲项目,我这里主要讲的是预售库存,主要涉及到高并发设计,采用缓存如何解决数据不一致问题来描述,项目讲完就是问职业规划这些的,然后二面结束

盒马三面

  • 盒马三面依旧是电话面试,面试时长和二面差不多,内容也比较相似,都是问项目,职业规划,但是在此基础上,还问了这样的业务场景有助于解决一个什么样的场景,对当前行业又如何的影响和后续能做的优化,把技术跟业务更紧密的结合,并且考察如何从技术层面推动业务,最后面试官问我如果是上嘉编制(盒马旗下公司,但是不属于阿里集团编制)考不考虑,我当时回答不考虑,毕竟对于阿里来说还是外包,三面也就到此结束
  • 后来二面面试官给我打电话,告诉我,看中我的潜力,让我可以在冲一下阿里其他的BU,这次面试给的P5+,觉得他老板的要求还是比较严格,噼里啪啦说了一堆,也很感谢二面的面试官,在面试评价中给我的评价比较好,也让我在接下来的面试更有优势

第二次面试

  • 第二次面试是找前同事推得业务平台事业部-营销平台,大概在第一次面试后两个月,中间被菜鸟的人卡住卡了大概一个月,很蛋疼

阿里简历评估面试

  • 简历评估面试大概问题和盒马一面类似,但是即便我说redis只是简单应用,也还是问了我redis的几种数据结构和特性,分布式锁用redis的什么数据结构实现,然后还有threadLocal

阿里在线笔试评估

  • 两道在线编码题,最开始第一道让我手写雪花算法,写不出来,后面面试官就给我换了
  • 有3个独立的线程,一个只会输出A,一个只会输出L,一个只会输出I,在三个线程同时启动的情况下,请用合理的方式让他们按顺序打印ALIALI
  • 请用java实现以下shell脚本的功能
    cat /home/admin/logs/webx.log | grep “Login” | uniq -c | sort -nr
  • 这两道题既是考察多线程和io

阿里一面

  • 由于中间阿里大部分BU锁了P7以下的hc,我的面试流程就被搁浅了,当时心想着估计今年够呛了,就在一个周五的中午,接到了一面电话
  • 一面内容和简历面差不多,还问了为dubbo贡献了什么代码,学习到了什么,怎么发现这些问题的,我就巴拉巴拉,另外还有就是如何快速定位问题,一台机器突然cpu和内存都满了,怎么样快速解决,这个时候考察的不是如果做gc调优,是如何快速进行问题修复
  • 然后一面就愉快地结束了

阿里二面

  • 阿里二面的时候是一个小姐姐,她应该就是我入职后的直属leader,她在一面的当天晚上就给我打来了电话,因为是小姐姐,最开始以为是HR,因为小姐姐的问题也都是职业规范,为什么离职,之前在公司怎么学习的,之前公司氛围很好为什么离职,后来又问我在dubbo贡献中学到了什么,自己觉得自己有什么优势,没有问一个技术,后来我问她,这个面试流程怎么回事,突然就hr面了吗,她说她不是hr,我说那怎么不问技术啊,比较惊讶, 她就告诉我说,前面的面试官已经问过技术了啊,大概聊一聊其实就知道了,就说我还不错,然后我问她如果又下一面是啥时候,她说大概一周左右吧,大老板比较忙

阿里三面

  • 过了周末,周日早上接到阿里hr小姐姐的面试邀约,周三早上在西溪园区进行现场终面,这一刻终于来了,准备好了阿里的入园前准备,一大早就去西溪园区面试了,这次面试的大佬也就是我们那个部门的CTO了
  • 面试首先自我介绍,然后讲述业务场景,为什么要这么做,然后这么做解决了什么问题,然后自己在项目中负责了什么,面试前,面试官告诉我说,一定不要紧张,他问的问题也许会有些刁钻,但是按照自己的想法,想清楚后回答就好了,不一定有正确答案,然后我依旧对之前的项目进行了架构描述,然后说自己重构后的架构,解决了什么问题,带来了怎么样的成果
  • 中途面试官说他去上个厕所,给我出了两个题目,让我在墙上画,一个是之前的架构我该如何优化,另一个是一个阶乘的两种写法(递归和非递归),阶乘题当时只写了一种,后来发现问题是自己没有做防御式编程,这个是不够细心,另外就是架构上后续优化,提出了可以在进行DDD上的实践
  • 最后面试官问我职业规划,然后离职原因这些,就和之前面试如出一辙了,面试大概持续了一个半小时,最后面试官给我介绍了我现在面试的是经济体解决方案这个部门,主要是负责从0到1的一些业务打包,为集团其他BU提供开箱即用的SDK

阿里HR面

  • 当天下午,阿里的hr又打电话约我第二天早上的HRG的视频面试
  • HRG的是面试大概就是为什么离职,职业规划,自己觉得自己的优势什么的,薪资待遇这些,大概时间半个小时,很快就结束了

收到Offer

  • 由于HRG面试是端午前,过了端午的星期一早上,HRG打来了电话,说我通过了面试,跟我讲了薪资和福利待遇,就愉快地接到Offer了

一些总结

  • 这两次阿里面试,其实可以看到阿里对于框架原理以及基础的比重非常高,不是背背面试题就能挺过去的
  • 另外对于业务上,阿里也喜欢问一些高并发的问题,并且如何解决大流量,保证系统可用性,然后以及架构的优化
  • 最后就是阿里很看重候选人的职业规划,我们需要对今后的职业发展有个很清晰的规划
  • 面试基础准备,这里有一份之前我的老大之一(现在在网易考拉)当时为我们分享的他这些年来认为java基础比较重要的地方,当然也是面试阿里必须要准备的一些知识点
  • java基础
  • 图片pdf的下载地址

感谢

  • 最后要感谢在我毕业后到面上至今给我帮助的朋友同事,如果没有你们用自己亲身经历踩过的坑的分享,给我一些很关键节点上的方向,我也不会拿到阿里的offer,感谢身边的所有人

Dubbo本地服务暴露的一些坑

Dubbo本地服务暴露的一些坑

本地暴露

  • Dubbo本地服务暴露,实在暴露远程服务之前会进行本地服务暴露,避免provider和consumer在同一容器内,进行不必要的网络开销,即本地调用对应protocol为injvm
  • 而在Dubbo的服务暴露中,应该默认是无论什么场景,都应该进行本地服务的暴露,但是实际情况却并不是这样,参照该issueinjvm and local call,本篇也主要对该issue的问题

ServiceConfig中exportLocal的问题

  • 如图,可以看到框中红线代码对我们配置中的协议做了判断,如果不是injvm协议,则构建一个injvm的的URL进行本地服务的暴露,那么问题来了,如果我们指定了injvm协议的话,这块将不会执行,那也就无法进行本地服务的暴露了,当然我们一般不会使用injvm协议进行本地调用,所以从dubbo开源到现在,这个问题也就没有被大家所发现,去掉这个判断逻辑便解决问题
  • 这块代码曾经在学习服务暴露的时候有过疑问,但是没有过于深究,直到看到这个issue的时候才恍然大悟,其实也反映出自己在学习框架源码过程中,对于一些细节有疑问但是却有惯性思维,觉得那么多人都看过这里,是自己理解错了吧的想法没有去仔细思考,学习还是需要带着自己的思想抱着一股打破砂锅问到底的劲才行

ServiceConfig中doExportUrls的问题

  • 如图,可以看到在往注册中心注册服务的时候,新增了如果是injvm协议,则跳出循环的判断,因为dubbo支持多注册中心,所以该判断不能再服务注册前过滤,在历史版本中,如果采用injvm协议,也会使该协议注册到registry上,这个显然是没有必要的

总结

  • 这两个问题都是很细微的问题,但是也反应出我们在平时编码中对细节的把控,已经对问题考虑的全面性,在学习的时候也同样要把控细节,不是说看看别人博客怎么写,自己就怎么想,源码学习应该有自己的思索

Netty HashedWheelTimer时间轮源码学习

HashedWheelTimer时间轮源码学习

HashedWheelTimer时间轮的简介

  • HashedWheelTimer是Netty中的一个基础工具类,主要用来高效处理大量定时任务,且任务对时间精度要求相对不高, 比如链接超时管理等场景, 缺点是, 内存占用相对较高。但是在使用时要注意任务里不要有太耗时的操作, 否则会阻塞Worker线程, 导致tick不准
  • HashedWheelTimer主要还是一个DelayQueue和一个时间轮算法组合
  • 如下图,可以看到HashedWheelTimer是由一个环形链表及数组构成
    HashedWheelTimer原理图
  • 如下图,可以解释为什么在使用HashedWheelTimer不能有太耗时的操作,因为worker的执行时,任务是串行的
    HashedWheelTimer执行过程
  • 如下图,可以看到HashedWheelTimer是由HashedWheelBucket数组, HashedWheelTimeout链表和工作线程Worker组成,所以我们的源码分析也主要从这几个类入手
    HashedWheelTimer类图

源码解析

HashedWheelTimer中的基本字段

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
//实例计数器
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
//实例过多警告值
private static final AtomicBoolean WARNED_TOO_MANY_INSTANCES = new AtomicBoolean();
//实际数量限制
private static final int INSTANCE_COUNT_LIMIT = 64;
private static final long MILLISECOND_NANOS = TimeUnit.MILLISECONDS.toNanos(1);
//资源泄漏检测器
private static final ResourceLeakDetector<HashedWheelTimer> leakDetector = ResourceLeakDetectorFactory.instance()
.newResourceLeakDetector(HashedWheelTimer.class, 1);
//工作线程状态更新
private static final AtomicIntegerFieldUpdater<HashedWheelTimer> WORKER_STATE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimer.class, "workerState");
//泄漏值
private final ResourceLeakTracker<HashedWheelTimer> leak;
//工作对象
private final Worker worker = new Worker();
//工作线程
private final Thread workerThread;

public static final int WORKER_STATE_INIT = 0;
public static final int WORKER_STATE_STARTED = 1;
public static final int WORKER_STATE_SHUTDOWN = 2;
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
private volatile int workerState; // 0 - init, 1 - started, 2 - shut down
//tick的时长,也就是指针多久转一格
private final long tickDuration;
//时间轮数组
private final HashedWheelBucket[] wheel;
// 这是一个标示符,用来快速计算任务应该呆的格子。
private final int mask;
//开始时间已初始化
private final CountDownLatch startTimeInitialized = new CountDownLatch(1);
//任务队列
private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();
//关闭的任务队列
private final Queue<HashedWheelTimeout> cancelledTimeouts = PlatformDependent.newMpscQueue();
//挂起超时次数
private final AtomicLong pendingTimeouts = new AtomicLong(0);
//最大挂起超时次数
private final long maxPendingTimeouts;
//开始时间
private volatile long startTime;
  • 从以上源码中我们可以大概了解到一个时间轮的执行依赖哪些条件,其中我们的任务都是基于Queue来实现的,但是这里我们要注意的是,这里的Queue是基于jctools中的Queue,以此得到更高的性能
  • mask标识符用来做位运算
  • 通过原子类来保证并发情况下的一致性
  • 这里我觉得值得我们学习的地方,是此处引用了资源泄露检测器,当资源超过64的时候就会进行告警,在细节方面netty考虑的非常全面,这个也是我们在平时编码的时需要学习的

HashedWheelTimer构造方法

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
public HashedWheelTimer(
ThreadFactory threadFactory, // 用来创建worker线程
long tickDuration,// tick的时长,也就是指针多久转一格
TimeUnit unit, // tickDuration的时间单位
int ticksPerWheel, // 一圈有几格
boolean leakDetection, // 是否开启内存泄露检测
long maxPendingTimeouts //最大挂起超时次数
) {

if (threadFactory == null) {
throw new NullPointerException("threadFactory");
}
if (unit == null) {
throw new NullPointerException("unit");
}
if (tickDuration <= 0) {
throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
}
if (ticksPerWheel <= 0) {
throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
}

// 将ticksPerWheel标准化为2的幂并初始化轮子.
wheel = createWheel(ticksPerWheel);
// 这是一个标示符,用来快速计算任务应该呆的格子。
// 我们知道,给定一个deadline的定时任务,其应该呆的格子=deadline%wheel.length.但是%操作是个相对耗时的操作,所以使用一种变通的位运算代替:
// 因为一圈的长度为2的n次方,mask = 2^n-1后低位将全部是1,然后deadline&mast == deadline%wheel.length
// java中的HashMap也是使用这种处理方法
mask = wheel.length - 1;

// 转换成纳秒处理
long duration = unit.toNanos(tickDuration);

// 校验是否存在溢出。即指针转动的时间间隔不能太长而导致tickDuration*wheel.length>Long.MAX_VALUE
if (duration >= Long.MAX_VALUE / wheel.length) {
throw new IllegalArgumentException(String.format(
"tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
tickDuration, Long.MAX_VALUE / wheel.length));
}

if (duration < MILLISECOND_NANOS) {
if (logger.isWarnEnabled()) {
logger.warn("Configured tickDuration %d smaller then %d, using 1ms.",
tickDuration, MILLISECOND_NANOS);
}
this.tickDuration = MILLISECOND_NANOS;
} else {
this.tickDuration = duration;
}

// 创建worker线程
workerThread = threadFactory.newThread(worker);

// 这里默认是启动内存泄露检测:当HashedWheelTimer实例超过当前cpu可用核数*4的时候,将发出警告
leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;

this.maxPendingTimeouts = maxPendingTimeouts;

if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
//发起警告
reportTooManyInstances();
}
}
  • 这里要注意,如果ticksPerWheel的默认值是512
  • HashedWheelTimer其实最终都是转换成纳秒处理的

HashedWheelTimer的createWheel方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
if (ticksPerWheel <= 0) {
throw new IllegalArgumentException(
"ticksPerWheel must be greater than 0: " + ticksPerWheel);
}
if (ticksPerWheel > 1073741824) {
throw new IllegalArgumentException(
"ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
}
// 初始化ticksPerWheel的值为不小于ticksPerWheel的最小2的n次方
ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
// 初始化wheel数组
HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
for (int i = 0; i < wheel.length; i ++) {
wheel[i] = new HashedWheelBucket();
}
return wheel;
}
  • 这里要注意,创建时间轮数组的时候,最大长度不能超过2的30次方

HashedWheelTimer的normalizeTicksPerWheel方法

1
2
3
4
5
6
7
8
// 初始化ticksPerWheel的值为不小于ticksPerWheel的最小2的n次方
private static int normalizeTicksPerWheel(int ticksPerWheel) {
int normalizedTicksPerWheel = 1;
while (normalizedTicksPerWheel < ticksPerWheel) {
normalizedTicksPerWheel <<= 1;
}
return normalizedTicksPerWheel;
}
  • 这里通过位运算来初始化每个轮盘的刻度
  • 但这里有个问题,如果轮盘大小指定过大,这里的循环次数也会更多,性能会存在问题,此处可以进行优化[jdk1.8 hashmap的hash算法,后面深入了解下]

HashedWheelTimer的start方法(时间轮启动的方法)

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
// 启动时间轮。这个方法其实不需要显示的主动调用,因为在添加定时任务(newTimeout()方法)的时候会自动调用此方法。
// 这个是合理的设计,因为如果时间轮里根本没有定时任务,启动时间轮也是空耗资源
public void start() {
// 判断当前时间轮的状态,如果是初始化,则启动worker线程,启动整个时间轮;如果已经启动则略过;如果是已经停止,则报错
// 这里是一个Lock Free的设计。因为可能有多个线程调用启动方法,这里使用AtomicIntegerFieldUpdater原子的更新时间轮的状态
switch (WORKER_STATE_UPDATER.get(this)) {
//如果时间轮还没有启动,则更改状态并启动
case WORKER_STATE_INIT:
if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
workerThread.start();
}
break;
//如果当前时间轮已经启动,则跳出该逻辑
case WORKER_STATE_STARTED:
break;
//如果是关闭状态,抛出无法启动异常
case WORKER_STATE_SHUTDOWN:
throw new IllegalStateException("cannot be started once stopped");
//如果工作状态未指定,则表示该程序异常,直接error
default:
throw new Error("Invalid WorkerState");
}

// 等待worker线程初始化时间轮的启动时间
while (startTime == 0) {
try {
startTimeInitialized.await();
} catch (InterruptedException ignore) {
// Ignore - it will be ready very soon.
}
}
}

HashedWheelTimer的stop方法(时间轮停止的方法)

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
public Set<Timeout> stop() {
// worker线程不能停止时间轮,也就是加入的定时任务,不能调用这个方法。
// 不然会有恶意的定时任务调用这个方法而造成大量定时任务失效
if (Thread.currentThread() == workerThread) {
throw new IllegalStateException(
HashedWheelTimer.class.getSimpleName() +
".stop() cannot be called from " +
TimerTask.class.getSimpleName());
}
// 尝试CAS替换当前状态为“停止:2”。如果失败,则当前时间轮的状态只能是“初始化:0”或者“停止:2”。直接将当前状态设置为“停止:2“
if (!WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_STARTED, WORKER_STATE_SHUTDOWN)) {
// workerState can be 0 or 2 at this moment - let it always be 2.
if (WORKER_STATE_UPDATER.getAndSet(this, WORKER_STATE_SHUTDOWN) != WORKER_STATE_SHUTDOWN) {
INSTANCE_COUNTER.decrementAndGet();
if (leak != null) {
boolean closed = leak.close(this);
assert closed;
}
}

return Collections.emptySet();
}

try {
boolean interrupted = false;
//如果工作线程存活
while (workerThread.isAlive()) {
//中断工作线程
//interrupt()不能中断在运行中的线程,它只能改变中断状态而已。
workerThread.interrupt();
try {
//工作线程加入本地线程
workerThread.join(100);
} catch (InterruptedException ignored) {
interrupted = true;
}
}
//如果发现线程已经被打上中断标识
if (interrupted) {
//改变当前线程状态
Thread.currentThread().interrupt();
}
} finally {
INSTANCE_COUNTER.decrementAndGet();
if (leak != null) {
boolean closed = leak.close(this);
assert closed;
}
}
return worker.unprocessedTimeouts();
}

HashedWheelTimer的newTimeout方法

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
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
// 参数校验
if (task == null) {
throw new NullPointerException("task");
}
if (unit == null) {
throw new NullPointerException("unit");
}

long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();
//待处理超时数 pendingTimeoutsCount 大于或等于允许的最大挂起
if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
pendingTimeouts.decrementAndGet();
throw new RejectedExecutionException("Number of pending timeouts ("
+ pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
+ "timeouts (" + maxPendingTimeouts + ")");
}
// 如果时间轮没有启动,则启动
start();

//将超时添加到超时队列,该队列将在下一个时钟处理。
//在处理过程中,所有排队的HashedWheelTimeouts都将添加到正确的HashedWheelBucket中。
long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

// 防止溢出。
if (delay > 0 && deadline < 0) {
deadline = Long.MAX_VALUE;
}
// 这里定时任务不是直接加到对应的格子中,而是先加入到一个队列里,然后等到下一个tick的时候,会从队列里取出最多100000个任务加入到指定的格子中
HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
timeouts.add(timeout);
return timeout;
}

Worker类

Worker是时间轮的核心线程类。tick的转动,过期任务的处理都是在这个线程中处理的。我们可以看到Worker实现Runnable接口,也就意味着我们的时间轮中是由worker来创建线程并执行任务

1
2
3
4
5
6
private final class Worker implements Runnable {
private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();

private long tick;
//... 省略方法
}

Worker类中的run方法

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
@Override
public void run() {
// 初始化startTime.只有所有任务的的deadline都是想对于这个时间点
startTime = System.nanoTime();
if (startTime == 0) {
// 由于System.nanoTime()可能返回0,甚至负数。并且0是一个标示符,用来判断startTime是否被初始化,所以当startTime=0的时候,重新赋值为1
startTime = 1;
}

// 唤醒阻塞在start()的线程
startTimeInitialized.countDown();
// 只要时间轮的状态为WORKER_STATE_STARTED,就循环的“转动”tick,循环判断响应格子中的到期任务
do {
// waitForNextTick方法主要是计算下次tick的时间, 然后sleep到下次tick
// 返回值就是System.nanoTime() - startTime, 也就是Timer启动后到这次tick, 所过去的时间
final long deadline = waitForNextTick();
if (deadline > 0) { // 可能溢出或者被中断的时候会返回负数, 所以小于等于0不管
// 获取tick对应的格子索引
int idx = (int) (tick & mask);
// 移除被取消的任务
processCancelledTasks();
HashedWheelBucket bucket =
wheel[idx];
// 从任务队列中取出任务加入到对应的格子中
transferTimeoutsToBuckets();
System.out.println("bucket"+bucket+",idx"+idx);
// 过期执行格子中的任务
bucket.expireTimeouts(deadline);
tick++;
}
} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

// 这里应该是时间轮停止了,清除所有格子中的任务,并加入到未处理任务列表,以供stop()方法返回
for (HashedWheelBucket bucket: wheel) {
bucket.clearTimeouts(unprocessedTimeouts);
}
// 将还没有加入到格子中的待处理定时任务队列中的任务取出,如果是未取消的任务,则加入到未处理任务队列中,以供stop()方法返回
for (;;) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
break;
}
if (!timeout.isCancelled()) {
unprocessedTimeouts.add(timeout);
}
}
// 处理取消的任务
processCancelledTasks();
}

Worker类中的transferTimeoutsToBuckets方法

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
// 将newTimeout()方法中加入到待处理定时任务队列中的任务加入到指定的格子中
private void transferTimeoutsToBuckets() {
// 每次tick只处理10w个任务,以免阻塞worker线程
// adds new timeouts in a loop.
for (int i = 0; i < 100000; i++) {
HashedWheelTimeout timeout = timeouts.poll();
//System.out.println("当前times.size"+timeouts.size());
// 如果没有任务了,直接跳出循环
if (timeout == null) {
// all processed
break;
}
// 还没有放入到格子中就取消了,直接略过
if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
// Was cancelled in the meantime.
continue;
}
// 计算任务需要经过多少个tick
long calculated = timeout.deadline / tickDuration;
// 计算任务的轮数
timeout.remainingRounds = (calculated - tick) / wheel.length;

//如果任务在timeouts队列里面放久了, 以至于已经过了执行时间, 这个时候就使用当前tick, 也就是放到当前bucket, 此方法调用完后就会被执行.
final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
System.out.println("tick:"+ticks);
int stopIndex = (int) (ticks & mask);
// 将任务加入到响应的格子中
HashedWheelBucket bucket = wheel[stopIndex];
bucket.addTimeout(timeout);
}
}b

Dubbo 服务引用

Dubbo 服务引用

服务引用

  • 大家都知道Dubbo是由consumer,provider,registry这三大部分组成
  • 那么consumer是如何发现provider并调用的呢,就是通过服务引用来实现的,也就是通过发现服务,然后进行调用

服务引用的流程

  • dubbo服务引用的流程大概如上图,不难发现其流程跟dubbo服务暴露互逆,(关于Dubbo服务暴露Dubbo服务暴露)但最终也是通过invoker来完成我们服务引用
  • dubbo服务引用最终通过ProxyFactory将Invoker转化为调用的Service
  • dubbo服务引用过程与dubbo服务暴露相似,都是通过SPI,适配相应的协议,并将服务注册到注册中心,并最终完成服务引用

源码解析

初始化

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 class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {

//省略一部分代码

//获取服务接口
@Override
public Object getObject() {
return get();
}

@Override
@SuppressWarnings({"unchecked"})
public void afterPropertiesSet() throws Exception {
//此处省略 配置校验代码
Boolean b = isInit();
if (b == null && getConsumer() != null) {
b = getConsumer().isInit();
}
if (b != null && b) {
//发现服务
getObject();
}
}
}
  • 首先我们来看一下ReferenceBean, ReferenceBean实现了InitializingBean, ApplicationContextAware, ApplicationListener这里同服务暴露一样,通过spring在初始化的时候进行服务引用

服务引用

  • 我们看到这里都调用了getObject()方法,其实是调用了ReferenceConfig中的get()方法,接下来我们一起看下ReferenceConfig中的get()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
public synchronized T get() {
//配置校验
checkAndUpdateSubConfigs();
//如果该服务已被销毁,则抛出异常
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
}
//如果服务为空,则进行初始化,否则直接返回
if (ref == null) {
init();
}
return ref;
}
  • 这里看到ReferenceConfig.get方法上加了一个锁,用来保证不会重复发现服务,而该方法的核心在于init()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void init() {
if (initialized) {
return;
}
initialized = true;
checkStubAndLocal(interfaceClass);
//校验mock
checkMock(interfaceClass);
Map<String, String> map = new HashMap<String, String>();

//省略对参数解析设置 ...

//创建代理对象
ref = createProxy(map);

ApplicationModel.initConsumerModel(getUniqueServiceName(), buildConsumerModel(attributes));
}
  • 这里通过对参数的解析来创建服务代理, createProxy()方法是整个服务引用初始化的关键
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
 private T createProxy(Map<String, String> map) {
URL tmpUrl = new URL("temp", "localhost", 0, map);
final boolean isJvmRefer;
if (isInjvm() == null) {
if (url != null && url.length() > 0) { // 如果指定了url,则不要进行本地引用
isJvmRefer = false;
} else {
// 默认情况下,引用本地服务(如果有)
isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl);
}
} else {
isJvmRefer = isInjvm();
}

if (isJvmRefer) {
URL url = new URL(Constants.LOCAL_PROTOCOL, Constants.LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
invoker = refprotocol.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
} else {
if (url != null && url.length() > 0) { // 用户指定的URL,可以是对等地址,也可以是注册中心的地址.
String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName);
}
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
} else {
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else { // x来自注册中心配置的URL
checkRegistry();
List<URL> us = loadRegistries(false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
}
//这里的refprotocol.refer即通过registryProtocol来进行发现
if (urls.size() == 1) {
invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // 使用最后一个注册表网址
}
}
if (registryURL != null) { // 注册表网址可用
// 仅在寄存器的群集可用时才使用RegistryAwareCluster
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, RegistryAwareCluster.NAME);
//调用者包装关系将是:RegistryAwareClusterInvoker(StaticDirectory) - > FailoverClusterInvoker(RegistryDirectory,将执行路由) - > Invoker invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // 不是注册表网址,必须直接调用。
//这里要注意 cluster 最终都会被包装成 MockClusterWrapper(SPI的依赖注入)
invoker = cluster.join(new StaticDirectory(invokers));
}
}
}

Boolean c = check;
if (c == null && consumer != null) {
c = consumer.isCheck();
}
if (c == null) {
c = true; // default true
}
if (c && !invoker.isAvailable()) {
// 如果提供者暂时不可用,则允许消费者稍后重试
initialized = false;
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
/**
* @since 2.7.0
* ServiceData Store
*/
MetadataReportService metadataReportService = null;
if ((metadataReportService = getMetadataReportService()) != null) {
URL consumerURL = new URL(Constants.CONSUMER_PROTOCOL, map.remove(Constants.REGISTER_IP_KEY), 0, map.get(Constants.INTERFACE_KEY), map);
metadataReportService.publishConsumer(consumerURL);
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}
  • 这里可以看到dubbo在服务引用中也可以使用本地服务的发现,但是可看到这一块已经被标记为过时,我的理解是dubbo作为一个RPC框架,本地服务还通过dubbo去调用,肯定与dubbo本身的意义不相匹配,所以便不推荐使用
  • 这块代码我们可以发现同服务暴露一样,会将consumer注册到所有配置的注册中心上去,而refprotocol.refer则是服务引用的核心代码
  • cluster对invoker进行了一层包装,以便应对后续服务调用中出现的异常情况进行处理
  • 最后我们的invoker将通过代理工厂转换为可以调用的代理服务

RegistryProtocal中的refer

RegistryProtocal
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
@Override
@SuppressWarnings("unchecked")
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = url.setProtocol(url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY)).removeParameter(REGISTRY_KEY);
//获取注册中心
Registry registry = registryFactory.getRegistry(url);
//如果是注册中心的服务,直接返回注册中心类型的invoker
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}

// group="a,b" or group="*"
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(Constants.GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(getMergeableCluster(), registry, type, url);
}
}
//发现服务
return doRefer(cluster, registry, type, url);
}

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
//创建并设置注册目录对象
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// all attributes of REFER_KEY
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
//注册服务
registry.register(directory.getRegisteredConsumerUrl());
}
directory.buildRouterChain(subscribeUrl);
//订阅服务
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
//装饰Invoker
Invoker invoker = cluster.join(directory);
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}
  • 在RegistryProtocal中,我们看到了cluster.join(directory),在ReferenceConfig中也出现过,在ReferenceConfig中没有注册中心的时候将直接使用装饰invoker,以供我们接下来服务调用来做集群容错
  • 服务引用在RegistryProtocal中的核心方法即为doRefer方法

RegistryDirectory

RegistryDirectory
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
/**
* 将网址转换为调用者,如果网址已被引用,则不会重新引用。
*
* @param urls
* @return invokers
*/
private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<>();
if (urls == null || urls.isEmpty()) {
return newUrlInvokerMap;
}
Set<String> keys = new HashSet<>();
String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
for (URL providerUrl : urls) {
// 如果在参考侧配置协议,则仅选择匹配协议
if (queryProtocols != null && queryProtocols.length() > 0) {
boolean accept = false;
String[] acceptProtocols = queryProtocols.split(",");
for (String acceptProtocol : acceptProtocols) {
if (providerUrl.getProtocol().equals(acceptProtocol)) {
accept = true;
break;
}
}
if (!accept) {
continue;
}
}
if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
continue;
}
if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() +
" in notified url: " + providerUrl + " from registry " + getUrl().getAddress() +
" to consumer " + NetUtils.getLocalHost() + ", supported protocol: " +
ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
continue;
}
URL url = mergeUrl(providerUrl);

String key = url.toFullString(); // 参数URL已排序
if (keys.contains(key)) { //重复的网址
continue;
}
keys.add(key);
// 缓存键是不与消费者方参数合并的URL,无论消费者如何组合参数,如果服务器URL更改,则再次引用
Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // 本地发现
Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
if (invoker == null) { // 不在缓存中,请再次发现
try {
boolean enabled = true;
if (url.hasParameter(Constants.DISABLED_KEY)) {
enabled = !url.getParameter(Constants.DISABLED_KEY, false);
} else {
enabled = url.getParameter(Constants.ENABLED_KEY, true);
}
if (enabled) {
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
}
} catch (Throwable t) {
logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
}
if (invoker != null) { // Put new invoker in cache
newUrlInvokerMap.put(key, invoker);
}
} else {
newUrlInvokerMap.put(key, invoker);
}
}
keys.clear();
return newUrlInvokerMap;
}
  • 那我们的服务最后是如何通相应协议打开consumer和provider的链接呢,关键代码就在RegistryDirectory的toInvokers方法,将url转换成具体的invoker,这个方法在订阅服务的时候会被触发,并且这里做了一层缓存,防止服务被多次引用

DubboProtocal中的refer

DubboProtocol
1
2
3
4
5
6
7
8
9
10
@Override
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url);

// create rpc invoker.
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);

return invoker;
}
  • 这里我们以Dubbo协议为例,看到DubboProtocal中的refer很简单,就是创建一个netty客户端,与provider进行连接返回一个Invoker即完成了一次服务的引用
  • 最后通过ProxyFactory的字节码结束,生成代理的可供调用的服务,到这里dubbo服务引用的流程就结束了,可以看出服务引用与服务暴露的过程中有很多类似的地方,其中还有很多细节没有展开,这也将是后续学习的重点

Dubbo 服务暴露

Dubbo 服务暴露

服务暴露

  • 大家都知道Dubbo是由consumer,provider,registry这三大部分组成
  • 那么provider的如何将服务提供给consumer调用呢,就是通过服务暴露来实现的,也就是把我们原来单机架构中的接口,对外部暴露

服务暴露的流程

  • dubbo服务暴露的流程大概如上图,在dubbo中,所有的服务都会被包装成一个invoker,这一点也将贯穿今后整个学习
  • dubbo服务暴露可以理解为两部分:本地暴露,远程暴露
  • 本地暴露的接口通常用于我们直接invoke dubbo的接口,以及有些时候我们的服务既是provider又是consumer,避免远程调用造成的资源浪费
  • 远程暴露则是将服务信息注册到registry,并且将服务通过网络提供给其他应用调用

源码解析

初始化

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
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware {
private static final long serialVersionUID = 213195494150089726L;
/**
*此处省略其他代码
**/

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
//服务暴露
export();
}
}

@Override
@SuppressWarnings({"unchecked", "deprecation"})
public void afterPropertiesSet() throws Exception {
//此处省略....
if (!supportedApplicationListener) {
//服务暴露
export();
}
}
}
  • 首先我们来看一下ServiceBean,ServiceBean实现了InitializingBean, ApplicationContextAware, ApplicationListener有没有觉得很熟悉,实现这几个类就能在spring初始化的时候do something

暴露服务

  • 我们看到这里都调用了export()方法
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 synchronized void export() {
if (provider != null) {
if (export == null) {
export = provider.getExport();
}
if (delay == null) {
delay = provider.getDelay();
}
}
if (export != null && !export) {
return;
}

if (delay != null && delay > 0) {
delayExportExecutor.schedule(new Runnable() {
@Override
public void run() {
doExport();
}
}, delay, TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
  • 这里看到ServiceConfig.export方法上加了一个锁,用来保证不会重复暴露服务,抛开上面的逻辑判断,在第一次初始化的时候,是直接走到了doExport()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
protected synchronized void doExport() {
//省略判断代码
doExportUrls();
}

private void doExportUrls() {
//加载注册中心的配置
List<URL> registryURLs = loadRegistries(true);
//把使用的协议注册到注册中心
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
  • 这里dubbo支持多协议,可以看到通过for循环可以把配置的多种协议都导出,进行暴露
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
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
//省略判断代码
// 导出服务
String contextPath = protocolConfig.getContextpath();
if ((contextPath == null || contextPath.length() == 0) && provider != null) {
contextPath = provider.getContextpath();
}

String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = this.findConfigedPorts(protocolConfig, name, map);
URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}

String scope = url.getParameter(Constants.SCOPE_KEY);
// 没有配置时不导出
if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {
// 导出本地服务
if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
exportLocal(url);
}
// 导出远程服务
if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
if (registryURLs != null && !registryURLs.isEmpty()) {
//将服务都注册到当前已有的注册中心上去
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
//判断是否有监控中心
URL monitorUrl = loadMonitor(registryURL);
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
if (logger.isInfoEnabled()) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
}
//对于providers,这用于启用自定义代理以生成invoker
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
//包装调用者和所有元数据的Invoker包装器
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
//没有注册中心直接暴露
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
//包装调用者和所有元数据的Invoker包装器
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
}
}
this.urls.add(url);
}

//暴露本地服务
private void exportLocal(URL url) {
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
//手动暴露一个本地服务
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(LOCALHOST)
.setPort(0);
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
}
}
  • 这你scope配置默认值是null,则本地服务和远程服务都导出,另外如果没有配置注册中心,将直接将接口暴露出去,我们可以根据自己所在的场景,选择都暴露还是指定暴露
  • 由于dubbo也是支持多注册中心的,所以可以通过for循环,将多个服务都注册到当前已有的注册中心上去
  • 在exportLocal方法这是将配置中解析好的url参数手动修改成本地协议进行服务暴露
  • ProxyFactory是通过SPI获取JavassistProxyFactory靠Javassist字节码技术动态的生成Invoker类,大家有兴趣的可以下去了解一下

暴露的细节

ProtocolFilterWrapper
1
2
3
4
5
6
7
8
9
10
11
public class ProtocolFilterWrapper implements Protocol {

private final Protocol protocol;
//装饰者模式
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
}
  • 在ServiceConfig中,通过SPI获取相应的Protocol,SPI中会对实现类进行装饰,每次执行protocol.exprot()方法的时候,其实都是执行的ProtocolFilterWrapper的protocol.exprot方法
ProtocolFilterWrapper
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
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
//如果是注册中心协议直接导出
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
//如果不是则执行整个filter的责任链
return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY, Constants.PROVIDER));
}

//责任链模式,对filter进行逐个执行
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
Invoker<T> last = invoker;
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
last = new Invoker<T>() {

@Override
public Class<T> getInterface() {
return invoker.getInterface();
}

@Override
public URL getUrl() {
return invoker.getUrl();
}

@Override
public boolean isAvailable() {
return invoker.isAvailable();
}

@Override
public Result invoke(Invocation invocation) throws RpcException {
Result result = filter.invoke(next, invocation);
if (result instanceof AsyncRpcResult) {
AsyncRpcResult asyncResult = (AsyncRpcResult) result;
asyncResult.thenApplyWithContext(r -> filter.onResponse(r, invoker, invocation));
return asyncResult;
} else {
return filter.onResponse(result, invoker, invocation);
}
}

@Override
public void destroy() {
invoker.destroy();
}

@Override
public String toString() {
return invoker.toString();
}
};
}
}
return last;
}
  • 由上述代码我们可以看到,dubbo中运用装饰者模式和责任链模式,对我们提供的服务做了一次封装,最终转换成我们需要的invoker对外暴露
  • 要注意到的是,当我们的协议是registry也就是注册协议的时候,是不需要进行构建责任链的

本地暴露

InjvmProtocol
1
2
3
4
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
  • 如果是本地暴露,则通过SPI拿到InjvmProtocol,最终通过injvm协议导出InjvmExporter

远程暴露

DubboProtocol
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
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
// 导出服务.
String key = serviceKey(url);
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
exporterMap.put(key, exporter);
//导出根服务以进行调度事件
Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
if (isStubSupportEvent && !isCallbackservice) {
String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
if (logger.isWarnEnabled()) {
logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
"], has set stubproxy support event ,but no stub methods founded."));
}
} else {
stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
}
}
//打开服务
openServer(url);
optimizeSerialization(url);
return exporter;
}

//打开服务
private void openServer(URL url) {
// find server.
String key = url.getAddress();
//客户端可以导出仅供服务器调用的服务
boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
if (isServer) {
ExchangeServer server = serverMap.get(key);
if (server == null) {
synchronized (this) {
server = serverMap.get(key);
if (server == null) {
//如果服务不存在,创建服务
serverMap.put(key, createServer(url));
}
}
} else {
// 服务器支持重置,与override一起使用
server.reset(url);
}
}
}
  • 在dubbo协议中,我们看到在服务导出的时候会根据配置地址,打开netty服务,也就是通过这一步,开启了RPC端口,使consumer通过TCP协议进行服务调用

服务注册

RegistryProtocol
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
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
URL registryUrl = getRegistryUrl(originInvoker);
// url在本地导出
URL providerUrl = getProviderUrl(originInvoker);
// 订阅覆盖数据
// 同样的服务由于订阅是带有服务名称的缓存密钥,因此会导致订阅信息覆盖。
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
//导出invoker
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
//将url注册到注册中心
final Registry registry = getRegistry(originInvoker);
final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
registryUrl, registeredProviderUrl);
//判断我们是否需要推迟发布
boolean register = registeredProviderUrl.getParameter("register", true);
if (register) {
register(registryUrl, registeredProviderUrl);
providerInvokerWrapper.setReg(true);
}
// Deprecated! Subscribe to override rules in 2.6.x or before.
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
//确保每次导出时都返回一个新的导出器实例
return new DestroyableExporter<>(exporter);
}

//真正导出服务的地方
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
String key = getCacheKey(originInvoker);
ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
if (exporter == null) {
synchronized (bounds) {
exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
if (exporter == null) {

final Invoker<?> invokerDelegete = new InvokerDelegate<T>(originInvoker, providerUrl);
//以dubbo协议为例,这里才是真正调用DubboProtocol.exprot的地方
exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
bounds.put(key, exporter);
}
}
}
return exporter;
}
  • 大家这里可以跟进源码,会发现,在ServiceConfig中通过proxyFactory生成的Invoker的url指向的协议其实是registry,所以在ServiceConfig中protocol.exprot调用的是RegistryProtocol的exprot方法
  • 在RegistryProtocol中调用了真正的远程服务暴露的方法,即DubboProtocol(以dubbo协议为例),在远程服务暴露成功后,将服务信息注册到registry上去,由此完成了一个服务的导出
  • 至此Dubbo服务暴露中的大致流程已经完成了,后面将会对Dubbo如何通过ProxyFactory生成Invoker,以及Registry是如何进行注册的进行更加深入的学习

JVM 锁优化

什么是锁优化

  • 为了线程之间更高效的共享数据,以及解决竞争问题,在JDK1.5之后,对锁进行了大量的优化,由此衍生出(自适应)自旋锁/轻量级锁/偏向级锁/锁消除/锁粗化等技术

锁消除

  • 锁消除是指虚拟机即时编译器在运行时,对一些代码上的同步要求,检测到是不可能存在共享数据竞争的,这时就会对锁进行清除,这里就好比我们去火车站买票需要排队,为了保证秩序,一般都会有一些围挡限制值人,但是现在不是高峰期,发现根本不需要做一些围栏来限制秩序,这个时候车站安保人员就会把围栏撤掉,这里这个围挡就相当于我们的锁,会根据实际情况来进行判断
  • 锁消除主要源于逃逸分析的数据支持,如果判断一段代码中,堆上的所有数据都不会逃逸出去被其他线程访问,那么可以把它当做栈上数据来对待,认为他是线程私有的,那么就不用加锁了
  • 要注意的是,不仅仅是我们在开发中手动加的锁,在有些场景中,同步代码也是普遍存在的
1
2
3
public String concat(String s1,String s2,String s3){
return s1 + s2 + s3 ;
}
  • 我们知道,由于String是一个不可变类,对字符串的操作是转换成新的String对象来进行的,在jdk1.5及以前的版本,这个字符串拼接会被javac编译成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//javac编译后
public String concat(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
//StringBuffer的append方法
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
  • 但是我们看到StringBuffer的append方法是一个同步方法,但是在该方法中,并不需要加锁,所以在jdk1.5以后的版本, StringBuffer会被优化成StringBuilder,而StringBuilder是不加锁的

锁粗化

  • 一般我们在开发过程中,推荐将同步块的作用域限制的尽量小,最好是只在共享数据的实际操作的作用域加锁,这样使得需要同步的操作数量尽可能小,如果存在锁竞争,那等待的线程也能够尽快拿到锁
  • 在大部分情况中上述原则是正确的,但是如果一些列连续都操作都是对同一个对象进行反复的加锁解锁操作,甚至在循环中出现这样的问题,那样既是没有线程的竞争,也会频繁的触发互斥同步的操作,对性能是有巨大损耗的
  • 如何解决这类问题呢,那么就是锁粗化,顾名思义,就是将细粒度的锁变成粗粒度的锁,如果虚拟机探测到有这样一系列零零碎碎的操作都对同一个对象加锁,会把这个锁同步范围扩大到整个操作序列的外部,以上面的concat方法为例,把原来每个append方法上的锁变成对concat方法的锁

自旋锁/自适应自旋锁

自旋锁

  • 提到自旋锁,我们先回到互斥同步本身,我们知道互斥同步有个很大的问题,就是阻塞实现,挂起线程和恢复线程都是内核态的操作,这些操作会给系统并发性能带来很大的压力,而且在很多场景中,共享数据的锁定只会持续很短的时间,而为了这段时间去恢复和挂起线程是非常不值得的,所以就有了自旋锁
  • 自旋锁,前提是机器有一核以上的处理器(一核的机器现在已经很少了,可以忽略这个前提),能让两个以上的线程并行执行,这个时候我们可以让后面请求锁的线程稍微等一会,看看持有锁的线程能否快速的释放这个锁,为了让线程陷入等待,就出现了自旋(说白了就是循环),在JUC包中我们可以看到AQS中出现的下面代码,就是基于CAS实现的自旋锁
1
2
3
for(;;){
doSomething();
}
  • 自旋锁虽然避免了线程切换带来的开销,但是在处理时间上却变得更长,如果锁占用的时间很短,那自旋等待的效果就很好,如果锁占用的时间长,那么自旋的线程只会白白浪费处理器的资源,而不会做其他有用的工作,反而会带来性能上的浪费,因此,在自旋到一定时间后还没有获得锁,就会将线程挂起,自旋的默认次数是10
  • 自适应自旋锁,顾名思义就是自动适配的自旋锁,它的自旋时间不在固定,而是由上一次同一个锁上的自旋时间和锁的拥有者的状态来决定的,由此可见JVM更加智能了,对于经常获得锁的,自旋的时间会尽可能的长,而对于自旋经常获取不到锁的线程,就直接挂起线程,避免资源的浪费

轻量级锁

轻量级锁

  • 轻量级锁也是jdk1.6之后引入的新型锁机制,轻量级是相对于使用操作系统的互斥量来实现的传统锁而言,因此传统的锁机制就被称为重量级锁,这里要强调一点的是,轻量级锁不是用来代替重量级锁的,它的本意是避免在没有锁竞争的情况下,使用重量级锁造成的资源浪费
  • 说到轻量级锁,首先要了解对象头的内容对象在JVM中的储存,对象在没有进入同步块的时候,如果此同步对象没有被锁定(锁标识为”01”的状态), jvm将在栈中建立一个LockRecord的空间,用于存储当前对象MarkWord的拷贝(Displaced MarkWord),如上图所示,然后虚拟机将使用CAS操作尝试对对象头的MarkWord更新为指向LockRecord的指针,如果更新成功,则对象拥有对该对象的锁,并且将锁标识转变成”00”
  • 如果更新失败了,jvm会首先检查对象的MarkWord是否指向当前的栈,如果指向当前线程的栈,则说明已经获得锁,那就可以进入同步块去执行,如果没有指向,则说明当前线程的锁已经被其他线程抢占了,如果有两条以上的线程去争夺同一个锁,那么轻量级锁就会失效,膨胀为重量级锁,锁标识的状态值也会变为”10”, MarkWord指向的就是重量级锁(互斥量的指针),后面等待锁的线程也要进入阻塞状态
  • 轻量级锁解锁也是通过CAS进行的,如果对象的MarkWord仍然指向着线程的锁记录,那就用CAS把对象当前的MarkWord替换回来,如果替换成功,说明整个同步过程就完成了,如果替换失败,说明其他线程尝试获取过锁,那么在释放锁的时候还要唤起其他被挂起的线程
  • 轻量级锁提升性能的依据在于,对于大部分的锁,在整个同步周期内都是不存在竞争的,这只是一个经验数据,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥线程的开销,但是如果存在竞争,那么除了互斥开销外,还有CAS的开销,在有竞争的情况下,轻量级锁会比传统的重量级锁更慢

偏向锁

偏向锁

  • 偏向锁和轻量级锁类似,也是对于MarkWord的一系列操作,顾名思义,偏向锁,是指偏向某一个线程的锁,偏向锁会偏向第一个获取他的线程,如果在接下来的执行当中都没有其他线程获取,那么持有偏向锁的线程将永远不用在进行同步
  • 当锁第一次被对象获取时,在将锁标识为”01”,即偏向锁模式,然后使用CAS操作把线程ID记录到MarkWord中,如果更新成功,持有偏向锁的线程,每次进入同步块时都不需要重新加锁
  • 当有另一个线程尝试获得锁时,偏向模式宣告结束,根据锁对象目前是否处于被锁定的状态,决定撤销偏向,还是升级到轻量级锁
  • 偏向锁可以提高带有同步但是无竞争的程序性能,同样是有利有弊,如果程序中大部分锁是要被多个不同线程访问,偏向锁肯定是有负担的

JVM 对象在内存中的存储

对象在内存中的存储

  • 如图1,可以看到一个对象在JVM内存中是如何存储的

图1

对象头(Header)

  • 对象头主要包括MarkWord,class指针,如果是数组还有数组的长度
  • MarkWord:包括对象的hashcode,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,如下图
    32位jvm中MarkWord
    64位jvm中MarkWord
  • 如上图所示,我们可以发现对象年龄分代为4bit,最大值也就是1111->15,所以在GC回收的时候,年龄超过15的对象会被加入老年代JVM新生代/老年代
  • 上图锁相关的标识将在后面的锁优化进行展开

实例数据(Instance Data)

  • 对象真正存储的有效数据,各种字段内容

对齐填充(Padding)

  • 不是必然存储,hotspot的自动内存管理要求对象大小必须是8字节的整数倍,而对象头正好是8字节的整数倍,所有实例数据部分如果没有对齐时,就需要通过对齐来自动填充

mysql innodb 表相关

innodb 表相关知识导图


JVM 新生代/老年代

JVM GC 流程图

图1

新生代

  • 主要是用来存放新生对象,一般占据对的1/3,由于频繁的创建对象,所以新生代会频繁的触发MinorGC进行垃圾回收
  • 新生代又分为Eden,SurvivorTo,SurvivorForm三个区
  • Eden区:JAVA新对象的出生地(如果新创建的对象占用内存很大,就直接分配到老年代),当Eden内存不够时,将触发一次MinorGC,对新生代进行一次回收,Eden占整个新生代的80%
  • SurvivorTo(SurvivorForm):保留了一次MinorGC的幸存者,占用新生代的10%
  • SurvivorForm(SurvivorTo):上一次MinorGC的幸存者,将和Eden一次参与这一次MinorGC扫描,占用新生代的10%
  • MinorGC的过程如果这次GC还幸存下来的对象,将复制到SurvivorTo中, SurvivorTo变成SurvivorForm,SurvivorForm变成SurvivorTo,并将对象年龄+1,这里就采用了标记-复制算法(由于新生代中都是朝生夕灭),如果发现SurvivorForm中的对象达到了老年标准,就把对象移动到老年代,一般年龄默认是15

老年代

  • 老年代主要存放的对象都比较稳定,一般存放的是应用程序中生命周期长的对象,当然也不排除有些朝生夕死的大对象,所以FullGC不会频繁的执行
  • 一般进行FullGC之前都会触发一次MinorGC,使得新对象晋升到老年代使得老年代内存不足而触发FullGC,或者是大对象直接进入老年代导致老年代内存不足而触发
  • FullGC采用标记-整理算法,因为每次GC后会造成大量的内存碎片,造成内存的不连续性,所以FullGC首先会先扫描一遍老年代,标记存活和要回收的对象,然后在进行整理,是存活的对象都移动到一端,最后直接对端边界的内存(回收对象)进行回收
  • 如果当老年代也装不下的时候就会抛出Out of Memeory(OOM)异常