From d46b8ac718669d0efb55c204c5681106d991bc08 Mon Sep 17 00:00:00 2001
From: nicksxs 模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件 模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件 using the Breadth-first traversal using the Breadth-first traversal sort the array, then test from head and end, until catch the right answer sort the array, then test from head and end, until catch the right answer vector中是全文匹配后的索引对,只是简单地用下。 vector中是全文匹配后的索引对,只是简单地用下。0%
\ No newline at end of file
+0%
\ No newline at end of file
diff --git a/2014/12/24/MFC 模态对话框/index.html b/2014/12/24/MFC 模态对话框/index.html
index 6f794e4ef7..255b9367fc 100644
--- a/2014/12/24/MFC 模态对话框/index.html
+++ b/2014/12/24/MFC 模态对话框/index.html
@@ -4,4 +4,4 @@
int nIndex = m_CbTest.GetCurSel();
m_CbTest.GetLBText(nIndex, m_SrcTest);
OnOK();
-}
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。0%
\ No newline at end of file
+}
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。0%
\ No newline at end of file
diff --git a/2014/12/30/Clone-Graph-Part-I/index.html b/2014/12/30/Clone-Graph-Part-I/index.html
index c75608d951..a64b12003d 100644
--- a/2014/12/30/Clone-Graph-Part-I/index.html
+++ b/2014/12/30/Clone-Graph-Part-I/index.html
@@ -34,4 +34,4 @@ Node *clone(Node *graph) {
}
return graphCopy;
-}anlysis
and use a map to save the neighbors not to be duplicated.0%
\ No newline at end of file
+}anlysis
and use a map to save the neighbors not to be duplicated.0%
\ No newline at end of file
diff --git a/2015/01/04/Path-Sum/index.html b/2015/01/04/Path-Sum/index.html
index 3174e12323..3a90b2bea5 100644
--- a/2015/01/04/Path-Sum/index.html
+++ b/2015/01/04/Path-Sum/index.html
@@ -31,4 +31,4 @@ public:
// DO NOT write int main() function
return deep_first_search(root, sum, 0);
}
-};0%
\ No newline at end of file
+};0%
\ No newline at end of file
diff --git a/2015/01/14/Two-Sum/index.html b/2015/01/14/Two-Sum/index.html
index dd54a875e2..0b59323578 100644
--- a/2015/01/14/Two-Sum/index.html
+++ b/2015/01/14/Two-Sum/index.html
@@ -47,4 +47,4 @@ public:
}
return result;
}
-};Analysis
0%
\ No newline at end of file
+};Analysis
0%
\ No newline at end of file
diff --git a/2015/01/16/pcre-intro-and-a-simple-package/index.html b/2015/01/16/pcre-intro-and-a-simple-package/index.html
index 52789bc1ee..9cd0163f80 100644
--- a/2015/01/16/pcre-intro-and-a-simple-package/index.html
+++ b/2015/01/16/pcre-intro-and-a-simple-package/index.html
@@ -15,4 +15,4 @@ int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, in
vc.push_back(pr);
rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
}
-}0%
\ No newline at end of file
+}0%
\ No newline at end of file
diff --git a/2015/03/11/Number-Of-1-Bits/index.html b/2015/03/11/Number-Of-1-Bits/index.html
index d7c979de06..949f1fb87a 100644
--- a/2015/03/11/Number-Of-1-Bits/index.html
+++ b/2015/03/11/Number-Of-1-Bits/index.html
@@ -12,4 +12,4 @@
n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits
return n;
-}0%
\ No newline at end of file
+}0%
\ No newline at end of file
diff --git a/2015/03/11/Reverse-Bits/index.html b/2015/03/11/Reverse-Bits/index.html
index 89bf57c8aa..d3b2bb8d1d 100644
--- a/2015/03/11/Reverse-Bits/index.html
+++ b/2015/03/11/Reverse-Bits/index.html
@@ -8,4 +8,4 @@ public:
n = ((n >> 16) & 0x0000ffff) | ((n & 0x0000ffff) << 16);
return n;
}
-};0%
\ No newline at end of file
+};0%
\ No newline at end of file
diff --git a/2015/03/13/Reverse-Integer/index.html b/2015/03/13/Reverse-Integer/index.html
index 324969b2af..36e5c24eb7 100644
--- a/2015/03/13/Reverse-Integer/index.html
+++ b/2015/03/13/Reverse-Integer/index.html
@@ -22,4 +22,4 @@ public:
}
return ret;
}
-};0%
\ No newline at end of file
+};0%
\ No newline at end of file
diff --git a/2015/04/14/Add-Two-Number/index.html b/2015/04/14/Add-Two-Number/index.html
index 48f1a5b0f4..7c24159553 100644
--- a/2015/04/14/Add-Two-Number/index.html
+++ b/2015/04/14/Add-Two-Number/index.html
@@ -81,4 +81,4 @@ public:
}
return dummy.next;
}
-};0%
\ No newline at end of file
+};0%
\ No newline at end of file
diff --git a/2015/04/15/Leetcode-No-3/index.html b/2015/04/15/Leetcode-No-3/index.html
index 5bdc55b4df..863073bb62 100644
--- a/2015/04/15/Leetcode-No-3/index.html
+++ b/2015/04/15/Leetcode-No-3/index.html
@@ -9,4 +9,4 @@
max = i - tail;
ct[s[i]] = i;
}
- return max;0%
\ No newline at end of file
+ return max;0%
\ No newline at end of file
diff --git a/2015/06/22/invert-binary-tree/index.html b/2015/06/22/invert-binary-tree/index.html
index b3faf91236..28f19fef87 100644
--- a/2015/06/22/invert-binary-tree/index.html
+++ b/2015/06/22/invert-binary-tree/index.html
@@ -27,4 +27,4 @@ public:
root->right = temp;
return root;
}
-};0%
\ No newline at end of file
+};0%
\ No newline at end of file
diff --git a/2016/07/13/swoole-websocket-test/index.html b/2016/07/13/swoole-websocket-test/index.html
index a7772bd564..4267d004eb 100644
--- a/2016/07/13/swoole-websocket-test/index.html
+++ b/2016/07/13/swoole-websocket-test/index.html
@@ -88,4 +88,4 @@ make && make install
本来只是想把 《1Q84》的读后感写下,现在觉得还是把这篇当成我今年的读书总结吧,不过先从《1Q84》说起。
-严格来讲,这不是很书面化的读后感,可能我想写的也只是像聊天一样的说下我读过的书,包括的技术博客其实也是类似的,以后或许会转变,但是目前水平如此吧,写多了可能会变好,也可能不会。
-开始正文吧,这书有点类似于海边的卡夫卡,一开始是通过两条故事线,穿插着叙述,一条是青豆的,不算是个职业杀手的女杀手,要去解决一个经常家暴的斯文败类,穿着描述得比较性感吧,杀人方式是通过比较长的细针,从脖子后面一个精巧的位置插入,可以造成是未知原因死亡的假象,可能会推断成心梗之类的,这里有个前置的细节,就是青豆是乘坐一辆很高级的出租车,内饰什么的都非常有质感,有点不像一辆出租车,然后车里放了一首比较小众的歌,雅纳切克的《小交响曲》,但是青豆知道它,这跟后面的情节也有些许关系,这是女主人公青豆的出场;相应的男主的出场印象不是太深刻,男主叫天吾,是个不知名的作家,跟一个叫小松的编辑有比较好的关系,虽然天吾还没有拿到比较有分量的奖项,但是小松很看好他,也让他帮忙审校一个新作家奖的投稿文章,虽然天吾自身还没获得过这个奖,天吾还有个正式工作,是当数学老师,天吾在学生时代是个数学天才,但后面有对文学产生了兴趣,文学还不足以养活自己,靠着教课还是能保持温饱;
-接下来是正式故事的起点了,就是小松收到了一部小说投稿,名叫《空气蛹》,是个叫深绘里的女孩子投的稿,小松对他赋予了很高的评价,这里好像记岔了,好像是天吾对这部小说很有好感,但是小松比较怀疑,然后小松看了之后也有了浓厚的兴趣,这里就是开端了,小松想让天吾来重写润色这部《空气蛹》,因为故事本身很有分量,但是描写手法叙事方式等都很拙劣,而天吾正好擅长这个,小松对天吾的评价是,描写技巧无可挑剔,就是故事主体的火花还没际遇迸发,需要一个导火索,这个就可以类比我们程序员,很多比较初中级的程序员主要擅长在原来的代码上修修改改或者给他分配个小功能,比较高级的程序员就需要能做一些项目的架构设计,核心的技术方案设计,以前我也觉得写文档这个比较无聊,但是当一个项目真的比较庞大,复杂的时候,整体和核心部分的架构设计和方案还是需要有文档沉淀的,不然别人不知道没法接受,自己过段时间也会忘记。
-对于小松的这个建议,他的初衷是想搅一搅这个死气沉沉套路颇深的文坛,因为本身《空气蛹》这部小说的内容很吸引人,小松想通过天吾的润色补充让这部小说冲击新人奖,有种恶作剧的意图,天吾对此表示很多担心和顾虑,小松的这个建议其实也是一种文学作假,有两方面的担心,一方面是原作者深绘里是否同意如此操作,一方面是外界如果发现了这个事实会有什么样的后果,但是小松表示不用担心,前一步由小松牵线,让天吾跟原作者深绘里当面沟通这个代写是否被允许,结果当然是被允许了,这里有了对深绘里的初步描写,按我的理解是比较仙的感觉,然后语言沟通有些吃力,或者说有她自己的特色,当面沟通时貌似是让深绘里回去再考虑下,然后后面再由天吾去深绘里寄宿的戎野老师家沟通具体的细节。
-2019年12月18日23:37:19 更新
去到戎野老师家之后,天吾知道了关于深绘里的一些事情,深绘里的父亲与戎野老师应该是老友,深绘里的父亲在当初成立了一个叫”先驱”的公社,一个独立运行的社会组织,以运营农场作为物资来源,追求更为松散的共同体,即不过分激进地公有制,进行松散的共同生活,承认私有财产,简而言之就是这样一个能稳定存活下来的独立社会组织,但是随着稳定运行,内部的激进派和稳健派开始出现分歧,不可磨合,后来两派就分裂了,深绘里的父亲,深田保留在了稳健派,但是此时其实深田保内心是矛盾的,以为一开始其实是他倡导的独立革命才组织起了这群人,然而现在他又认清了现实社会已经不太相信能通过革命来独立的可能性,后来激进派便开始越加封闭,而且进行军事训练和思想教育,而后这个先驱的激进派别便有了新的名字”黎明”,深绘里也是在此时从先驱逃离来投靠戎野老师
暂时先写到这,未完待续~
本来只是想把 《1Q84》的读后感写下,现在觉得还是把这篇当成我今年的读书总结吧,不过先从《1Q84》说起。
+严格来讲,这不是很书面化的读后感,可能我想写的也只是像聊天一样的说下我读过的书,包括的技术博客其实也是类似的,以后或许会转变,但是目前水平如此吧,写多了可能会变好,也可能不会。
+开始正文吧,这书有点类似于海边的卡夫卡,一开始是通过两条故事线,穿插着叙述,一条是青豆的,不算是个职业杀手的女杀手,要去解决一个经常家暴的斯文败类,穿着描述得比较性感吧,杀人方式是通过比较长的细针,从脖子后面一个精巧的位置插入,可以造成是未知原因死亡的假象,可能会推断成心梗之类的,这里有个前置的细节,就是青豆是乘坐一辆很高级的出租车,内饰什么的都非常有质感,有点不像一辆出租车,然后车里放了一首比较小众的歌,雅纳切克的《小交响曲》,但是青豆知道它,这跟后面的情节也有些许关系,这是女主人公青豆的出场;相应的男主的出场印象不是太深刻,男主叫天吾,是个不知名的作家,跟一个叫小松的编辑有比较好的关系,虽然天吾还没有拿到比较有分量的奖项,但是小松很看好他,也让他帮忙审校一个新作家奖的投稿文章,虽然天吾自身还没获得过这个奖,天吾还有个正式工作,是当数学老师,天吾在学生时代是个数学天才,但后面有对文学产生了兴趣,文学还不足以养活自己,靠着教课还是能保持温饱;
+接下来是正式故事的起点了,就是小松收到了一部小说投稿,名叫《空气蛹》,是个叫深绘里的女孩子投的稿,小松对他赋予了很高的评价,这里好像记岔了,好像是天吾对这部小说很有好感,但是小松比较怀疑,然后小松看了之后也有了浓厚的兴趣,这里就是开端了,小松想让天吾来重写润色这部《空气蛹》,因为故事本身很有分量,但是描写手法叙事方式等都很拙劣,而天吾正好擅长这个,小松对天吾的评价是,描写技巧无可挑剔,就是故事主体的火花还没际遇迸发,需要一个导火索,这个就可以类比我们程序员,很多比较初中级的程序员主要擅长在原来的代码上修修改改或者给他分配个小功能,比较高级的程序员就需要能做一些项目的架构设计,核心的技术方案设计,以前我也觉得写文档这个比较无聊,但是当一个项目真的比较庞大,复杂的时候,整体和核心部分的架构设计和方案还是需要有文档沉淀的,不然别人不知道没法接受,自己过段时间也会忘记。
+对于小松的这个建议,他的初衷是想搅一搅这个死气沉沉套路颇深的文坛,因为本身《空气蛹》这部小说的内容很吸引人,小松想通过天吾的润色补充让这部小说冲击新人奖,有种恶作剧的意图,天吾对此表示很多担心和顾虑,小松的这个建议其实也是一种文学作假,有两方面的担心,一方面是原作者深绘里是否同意如此操作,一方面是外界如果发现了这个事实会有什么样的后果,但是小松表示不用担心,前一步由小松牵线,让天吾跟原作者深绘里当面沟通这个代写是否被允许,结果当然是被允许了,这里有了对深绘里的初步描写,按我的理解是比较仙的感觉,然后语言沟通有些吃力,或者说有她自己的特色,当面沟通时貌似是让深绘里回去再考虑下,然后后面再由天吾去深绘里寄宿的戎野老师家沟通具体的细节。
+2019年12月18日23:37:19 更新
去到戎野老师家之后,天吾知道了关于深绘里的一些事情,深绘里的父亲与戎野老师应该是老友,深绘里的父亲在当初成立了一个叫”先驱”的公社,一个独立运行的社会组织,以运营农场作为物资来源,追求更为松散的共同体,即不过分激进地公有制,进行松散的共同生活,承认私有财产,简而言之就是这样一个能稳定存活下来的独立社会组织,但是随着稳定运行,内部的激进派和稳健派开始出现分歧,不可磨合,后来两派就分裂了,深绘里的父亲,深田保留在了稳健派,但是此时其实深田保内心是矛盾的,以为一开始其实是他倡导的独立革命才组织起了这群人,然而现在他又认清了现实社会已经不太相信能通过革命来独立的可能性,后来激进派便开始越加封闭,而且进行军事训练和思想教育,而后这个先驱的激进派别便有了新的名字”黎明”,深绘里也是在此时从先驱逃离来投靠戎野老师
暂时先写到这,未完待续~
这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。
+技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。
+另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。
+还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。
+下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。
+]]>工作没什么大变化,有了些微的提升,可能因为是来了之后做了些项目对比公司与来还算是比较重要的,但是技术难度上没有特别突出的点,可能最开始用 openresty+lua 做了个 ab 测的工具,还是让我比较满意的,后面一般都是业务型的需求,今年可能在业务相关的技术逻辑上有了一些深度的了解,而原来一直想做的业务架构升级和通用型技术中间件这样的优化还是停留在想象中,前面说的 ab 测应该算是个半成品,还是没能多走出这一步,得需要多做一些实在的事情,比如轻量级的业务框架,能够对原先不熟悉的业务逻辑,代码逻辑有比较深入的理解,而不是一直都是让特定的同学负责特定的逻辑,很多时候还是在偷懒,习惯以一些简单安全的方案去做事情,在技术上还是要有所追求,还有就是能够在新语言,主要是 rust,swift 这类的能有些小玩具可以做,rust 的话是因为今年看了一本相关的书,后面三分之一其实消化得不好,这本书整体来说是很不错的,只是 rust 本身在所有权这块,还有引用包装等方面是设计得比较难懂,也可能是我基础差,所以还是想在复习下,可以做一个简单的命令行工具这种,然后 swift 是想说可以做点 mac 的小软件,原生的毕竟性能好点,又小。基于 web 做的客户端大部分都是又丑又大,极少数能好看点,但也是很重,起码 7~80M 的大小,原生的估计能除以 10。
整体的职业规划貌似陷入了比较大的困惑期,在目前公司发展前景不是很大,但是出去貌似也没有比较适合我的机会,总的来说还是杭州比较卷,个人觉得有自己的时间是非常重要的,而且这个不光是用来自我提升的,还是让自己有足够的时间做缓冲,有足够的时间锻炼减肥,时间少的情况下,不光会在仅有的时间里暴饮暴食,还没空锻炼,身体是革命的本钱,现在其实能特别明显地感觉到身体状态下滑,容易疲劳,焦虑。所以是否也许有可能以后要往外企这类的方向去发展。
工作上其实还是有个不大不小的缺点,就是容易激动,容易焦虑,前一点可能有稍稍地改观,因为工作中的很多现状其实是我个人难以改变的,即使觉得不合理,但是结构在那里,还不如自己放宽心,尽量做好事情就行。第二点的话还是做得比较差,一直以来抗压能力都比较差,跟成长环境,家庭环境都有比较大的关系,而且说实在的特别是父母,基本也没有在这方面给我正向的帮助,比较擅长给我施压,从小就是通过压力让我好好读书,当个乖学生,考个好学校,并没有能真正地理解我的压力,教我或者帮助我解压,只会在那说着不着边际的空话,甚至经常反过来对我施压。还是希望能慢慢解开,这点可能对我身体也有影响,也许需要看一些心理疏导相关的书籍。工作篇暂时到这,后续还有其他篇,未完待续哈哈😀
这一年做的最让自己满意的应该就是看了一些书,由折腾群洋总发起的读书打卡活动,到目前为止已经读完了这几本书,《cUrl 必知必会》,《古董局中局 1》,《古董局中局 2》,《算法图解》,《每天 5 分钟玩转 Kubernetes》《幸福了吗?》《高可用可伸缩微服务架构:基于 Dubbo、Spring Cloud和 Service Mesh》《Rust 权威指南》后面可以写个专题说说看的这些书,虽然每天打卡如果时间安排不好,并且看的书像 rust 这样比较难的话还是会有点小焦虑,不过也是个调整过程,一方面可以在白天就抽空看一会,然后也不必要每次都看很大一章,注重吸收。
-技术上的成长的话,有一些比较小的长进吧,对于一些之前忽视的 synchronized,ThreadLocal 和 AQS 等知识点做了下查漏补缺了,然后多了解了一些 Java 垃圾回收的内容,但是在实操上还是比较欠缺,成型的技术方案,架构上所谓的优化也比较少,一些想法也还有考虑不周全的地方,还需要多花时间和心思去学习加强,特别是在目前已经有的基础上如何做系统深层次的优化,既不要是鸡毛蒜皮的,也不能出现一些不可接受的问题和故障,这是个很重要的课题,需要好好学习,后面考虑定一些周期性目标,两个月左右能有一些成果和总结。
-另外一部分是自己的服务,因为 ucloud 的机器太贵就没续费了,所以都迁移到腾讯云的小机器上了,顺便折腾了一点点 traefik,但是还很不熟练,不太习惯这一套,一方面是 docker 还不习惯,这也加重了对这套环境的不适应,还是习惯裸机部署,另一方面就是 k8s 了,家里的机器还没虚拟化,没有很好的条件可以做实验,这也是读书打卡的一个没做好的点,整体的学习效果受限于深度和实操,后面是看都是用 traefik,也找到了一篇文章可以 traefik 转发到裸机应用,因为主仓库用的是裸机的 gogs。
-还有就是运动减肥上,唉,这又是很大的一个痛点,基本没效果,只是还算稳定,昨天看到一个视频说还需要力量训练来增肌,以此可以提升基础代谢,打算往这个方向尝试下,因为今天没有疫情限制了,在 6 月底完成了 200 公里的跑步小目标,只是有些膝盖跟大腿根外侧不适,抽空得去看下医生,后面打算每天也能做点卷腹跟俯卧撑。
-下半年还希望能继续多看看书,比很多网上各种乱七八糟的文章会好很多,结合豆瓣评分,找一些评价高一些的文章,但也不是说分稍低点的就不行,有些也看人是不是适合,一般 6 分以上评价比较多的就可以试试。
-]]>工作没什么大变化,有了些微的提升,可能因为是来了之后做了些项目对比公司与来还算是比较重要的,但是技术难度上没有特别突出的点,可能最开始用 openresty+lua 做了个 ab 测的工具,还是让我比较满意的,后面一般都是业务型的需求,今年可能在业务相关的技术逻辑上有了一些深度的了解,而原来一直想做的业务架构升级和通用型技术中间件这样的优化还是停留在想象中,前面说的 ab 测应该算是个半成品,还是没能多走出这一步,得需要多做一些实在的事情,比如轻量级的业务框架,能够对原先不熟悉的业务逻辑,代码逻辑有比较深入的理解,而不是一直都是让特定的同学负责特定的逻辑,很多时候还是在偷懒,习惯以一些简单安全的方案去做事情,在技术上还是要有所追求,还有就是能够在新语言,主要是 rust,swift 这类的能有些小玩具可以做,rust 的话是因为今年看了一本相关的书,后面三分之一其实消化得不好,这本书整体来说是很不错的,只是 rust 本身在所有权这块,还有引用包装等方面是设计得比较难懂,也可能是我基础差,所以还是想在复习下,可以做一个简单的命令行工具这种,然后 swift 是想说可以做点 mac 的小软件,原生的毕竟性能好点,又小。基于 web 做的客户端大部分都是又丑又大,极少数能好看点,但也是很重,起码 7~80M 的大小,原生的估计能除以 10。
整体的职业规划貌似陷入了比较大的困惑期,在目前公司发展前景不是很大,但是出去貌似也没有比较适合我的机会,总的来说还是杭州比较卷,个人觉得有自己的时间是非常重要的,而且这个不光是用来自我提升的,还是让自己有足够的时间做缓冲,有足够的时间锻炼减肥,时间少的情况下,不光会在仅有的时间里暴饮暴食,还没空锻炼,身体是革命的本钱,现在其实能特别明显地感觉到身体状态下滑,容易疲劳,焦虑。所以是否也许有可能以后要往外企这类的方向去发展。
工作上其实还是有个不大不小的缺点,就是容易激动,容易焦虑,前一点可能有稍稍地改观,因为工作中的很多现状其实是我个人难以改变的,即使觉得不合理,但是结构在那里,还不如自己放宽心,尽量做好事情就行。第二点的话还是做得比较差,一直以来抗压能力都比较差,跟成长环境,家庭环境都有比较大的关系,而且说实在的特别是父母,基本也没有在这方面给我正向的帮助,比较擅长给我施压,从小就是通过压力让我好好读书,当个乖学生,考个好学校,并没有能真正地理解我的压力,教我或者帮助我解压,只会在那说着不着边际的空话,甚至经常反过来对我施压。还是希望能慢慢解开,这点可能对我身体也有影响,也许需要看一些心理疏导相关的书籍。工作篇暂时到这,后续还有其他篇,未完待续哈哈😀
// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
+private transient volatile Node head;
+
+// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
+private transient volatile Node tail;
+
+// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
+// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
+private volatile int state;
+
+// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
+// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
+// if (currentThread == getExclusiveOwnerThread()) {state++}
+private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
+大概了解了 aqs 底层的双向等待队列,
结构是这样的
每个 node 里面主要是的代码结构也比较简单
static final class Node {
+ // 标识节点当前在共享模式下
+ static final Node SHARED = new Node();
+ // 标识节点当前在独占模式下
+ static final Node EXCLUSIVE = null;
+
+ // ======== 下面的几个int常量是给waitStatus用的 ===========
+ /** waitStatus value to indicate thread has cancelled */
+ // 代码此线程取消了争抢这个锁
+ static final int CANCELLED = 1;
+ /** waitStatus value to indicate successor's thread needs unparking */
+ // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
+ static final int SIGNAL = -1;
+ /** waitStatus value to indicate thread is waiting on condition */
+ // 本文不分析condition,所以略过吧,下一篇文章会介绍这个
+ static final int CONDITION = -2;
+ /**
+ * waitStatus value to indicate the next acquireShared should
+ * unconditionally propagate
+ */
+ // 同样的不分析,略过吧
+ static final int PROPAGATE = -3;
+ // =====================================================
+
+
+ // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
+ // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
+ // ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
+ volatile int waitStatus;
+ // 前驱节点的引用
+ volatile Node prev;
+ // 后继节点的引用
+ volatile Node next;
+ // 这个就是线程本尊
+ volatile Thread thread;
+
+}
+其实可以主要关注这个 waitStatus 因为这个是后面的节点给前面的节点设置的,等于-1 的时候代表后面有节点等待,需要去唤醒,
这里使用了一个变种的 CLH 队列实现,CLH 队列相关内容可以查看这篇 自旋锁、排队自旋锁、MCS锁、CLH锁
EnableApolloConfig 注解
-@Import(ApolloConfigRegistrar.class)
-public @interface EnableApolloConfig {
-这个 import 实现了
-public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
-
- private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);
+ Apollo 如何获取当前环境
+ /2022/09/04/Apollo-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83/
+ 在用 Apollo 作为配置中心的过程中才到过几个坑,这边记录下,因为运行 java 服务的启动参数一般比较固定,所以我们在一个新环境里运行的时候没有特意去检查,然后突然发现业务上有一些数据异常,排查之后才发现java 服务连接了测试环境的 apollo,而原因是因为环境变量传了-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境
+public enum Env{
+ LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
- @Override
- public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
- helper.registerBeanDefinitions(importingClassMetadata, registry);
+ public static Env fromString(String env) {
+ Env environment = EnvUtils.transformEnv(env);
+ Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
+ return environment;
}
}
-
-然后就调用了
-com.ctrip.framework.apollo.spring.spi.DefaultApolloConfigRegistrarHelper#registerBeanDefinitions
-接着是注册了这个 bean,com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor
+而这些解释
+/**
+ * Here is the brief description for all the predefined environments:
+ * <ul>
+ * <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
+ * <li>DEV: Development environment</li>
+ * <li>FWS: Feature Web Service Test environment</li>
+ * <li>FAT: Feature Acceptance Test environment</li>
+ * <li>UAT: User Acceptance Test environment</li>
+ * <li>LPT: Load and Performance Test environment</li>
+ * <li>PRO: Production environment</li>
+ * <li>TOOLS: Tooling environment, a special area in production environment which allows
+ * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
+ * </ul>
+ */
+那如果要在运行时知道 apollo 当前使用的环境可以用这个
+Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
+简单记录下。
+]]>
+
+ Java
+
+
+ Java
+ Apollo
+ environment
+
+ com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField
@Override
-public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
- AnnotationAttributes attributes = AnnotationAttributes
- .fromMap(importingClassMetadata.getAnnotationAttributes(EnableApolloConfig.class.getName()));
- String[] namespaces = attributes.getStringArray("value");
- int order = attributes.getNumber("order");
- PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order);
-
- Map<String, Object> propertySourcesPlaceholderPropertyValues = new HashMap<>();
- // to make sure the default PropertySourcesPlaceholderConfigurer's priority is higher than PropertyPlaceholderConfigurer
- propertySourcesPlaceholderPropertyValues.put("order", 0);
-
+ protected void processField(Object bean, String beanName, Field field) {
+ // register @Value on field
+ Value value = field.getAnnotation(Value.class);
+ if (value == null) {
+ return;
+ }
+ Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
+
+ if (keys.isEmpty()) {
+ return;
+ }
+
+ for (String key : keys) {
+ SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
+ springValueRegistry.register(beanFactory, key, springValue);
+ logger.debug("Monitoring {}", springValue);
+ }
+ }
+然后我们看下这个springValueRegistry是啥玩意
public class SpringValueRegistry {
+ private static final long CLEAN_INTERVAL_IN_SECONDS = 5;
+ private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final Object LOCK = new Object();
+
+ public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
+ if (!registry.containsKey(beanFactory)) {
+ synchronized (LOCK) {
+ if (!registry.containsKey(beanFactory)) {
+ registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
+ }
+ }
+ }
+
+ registry.get(beanFactory).put(key, springValue);
+
+ // lazy initialize
+ if (initialized.compareAndSet(false, true)) {
+ initialize();
+ }
+ }
+这类其实就是个 map 来存放 springvalue,然后有com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener来监听更新操作,当有变更时
@Override
+ public void onChange(ConfigChangeEvent changeEvent) {
+ Set<String> keys = changeEvent.changedKeys();
+ if (CollectionUtils.isEmpty(keys)) {
+ return;
+ }
+ for (String key : keys) {
+ // 1. check whether the changed key is relevant
+ Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
+ if (targetValues == null || targetValues.isEmpty()) {
+ continue;
+ }
+
+ // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
+ // 这里其实有一点比较绕,是因为 Apollo 里的 namespace 划分,会出现 key 相同,但是 namespace 不同的情况,所以会有个优先级存在,所以需要去校验 environment 里面的是否已经更新,如果未更新则表示不需要更新
+ if (!shouldTriggerAutoUpdate(changeEvent, key)) {
+ continue;
+ }
+
+ // 3. update the value
+ for (SpringValue val : targetValues) {
+ updateSpringValue(val);
+ }
+ }
+ }
+其实原理很简单,就是得了解知道下
+]]>EnableApolloConfig 注解
+@Import(ApolloConfigRegistrar.class)
+public @interface EnableApolloConfig {
+这个 import 实现了
+public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
+
+ private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+ helper.registerBeanDefinitions(importingClassMetadata, registry);
+ }
+}
+
+然后就调用了
+com.ctrip.framework.apollo.spring.spi.DefaultApolloConfigRegistrarHelper#registerBeanDefinitions
+接着是注册了这个 bean,com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor
+@Override
+public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+ AnnotationAttributes attributes = AnnotationAttributes
+ .fromMap(importingClassMetadata.getAnnotationAttributes(EnableApolloConfig.class.getName()));
+ String[] namespaces = attributes.getStringArray("value");
+ int order = attributes.getNumber("order");
+ PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order);
+
+ Map<String, Object> propertySourcesPlaceholderPropertyValues = new HashMap<>();
+ // to make sure the default PropertySourcesPlaceholderConfigurer's priority is higher than PropertyPlaceholderConfigurer
+ propertySourcesPlaceholderPropertyValues.put("order", 0);
+
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(),
PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues);
// 注册了这个 bean
@@ -1352,93 +1562,6 @@ public:
Apollo
com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor#processField
-@Override
- protected void processField(Object bean, String beanName, Field field) {
- // register @Value on field
- Value value = field.getAnnotation(Value.class);
- if (value == null) {
- return;
- }
- Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
-
- if (keys.isEmpty()) {
- return;
- }
-
- for (String key : keys) {
- SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
- springValueRegistry.register(beanFactory, key, springValue);
- logger.debug("Monitoring {}", springValue);
- }
- }
-然后我们看下这个springValueRegistry是啥玩意
public class SpringValueRegistry {
- private static final long CLEAN_INTERVAL_IN_SECONDS = 5;
- private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
- private final AtomicBoolean initialized = new AtomicBoolean(false);
- private final Object LOCK = new Object();
-
- public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
- if (!registry.containsKey(beanFactory)) {
- synchronized (LOCK) {
- if (!registry.containsKey(beanFactory)) {
- registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
- }
- }
- }
-
- registry.get(beanFactory).put(key, springValue);
-
- // lazy initialize
- if (initialized.compareAndSet(false, true)) {
- initialize();
- }
- }
-这类其实就是个 map 来存放 springvalue,然后有com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener来监听更新操作,当有变更时
@Override
- public void onChange(ConfigChangeEvent changeEvent) {
- Set<String> keys = changeEvent.changedKeys();
- if (CollectionUtils.isEmpty(keys)) {
- return;
- }
- for (String key : keys) {
- // 1. check whether the changed key is relevant
- Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
- if (targetValues == null || targetValues.isEmpty()) {
- continue;
- }
-
- // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
- // 这里其实有一点比较绕,是因为 Apollo 里的 namespace 划分,会出现 key 相同,但是 namespace 不同的情况,所以会有个优先级存在,所以需要去校验 environment 里面的是否已经更新,如果未更新则表示不需要更新
- if (!shouldTriggerAutoUpdate(changeEvent, key)) {
- continue;
- }
-
- // 3. update the value
- for (SpringValue val : targetValues) {
- updateSpringValue(val);
- }
- }
- }
-其实原理很简单,就是得了解知道下
-]]>-Denv=fat,而在我们的环境配置中 fat 就是代表测试环境, 其实应该是-Denv=pro,而 apollo 总共有这些环境
-public enum Env{
- LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
+ Comparator使用小记
+ /2020/04/05/Comparator%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%AE%B0/
+ 在Java8的stream之前,将对象进行排序的时候,可能需要对象实现Comparable接口,或者自己实现一个Comparator,
+比如这样子
+我的对象是Entity
+public class Entity {
- public static Env fromString(String env) {
- Env environment = EnvUtils.transformEnv(env);
- Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
- return environment;
- }
-}
-而这些解释
-/**
- * Here is the brief description for all the predefined environments:
- * <ul>
- * <li>LOCAL: Local Development environment, assume you are working at the beach with no network access</li>
- * <li>DEV: Development environment</li>
- * <li>FWS: Feature Web Service Test environment</li>
- * <li>FAT: Feature Acceptance Test environment</li>
- * <li>UAT: User Acceptance Test environment</li>
- * <li>LPT: Load and Performance Test environment</li>
- * <li>PRO: Production environment</li>
- * <li>TOOLS: Tooling environment, a special area in production environment which allows
- * access to test environment, e.g. Apollo Portal should be deployed in tools environment</li>
- * </ul>
- */
-那如果要在运行时知道 apollo 当前使用的环境可以用这个
-Env apolloEnv = ApolloInjector.getInstance(ConfigUtil.class).getApolloEnv();
-简单记录下。
-]]>
-
- Java
-
+ private Long id;
+
+ private Long sortValue;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getSortValue() {
+ return sortValue;
+ }
+
+ public void setSortValue(Long sortValue) {
+ this.sortValue = sortValue;
+ }
+}
+
+Comparator
+public class MyComparator implements Comparator {
+ @Override
+ public int compare(Object o1, Object o2) {
+ Entity e1 = (Entity) o1;
+ Entity e2 = (Entity) o2;
+ if (e1.getSortValue() < e2.getSortValue()) {
+ return -1;
+ } else if (e1.getSortValue().equals(e2.getSortValue())) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+}
+
+比较代码
+private static MyComparator myComparator = new MyComparator();
+
+ public static void main(String[] args) {
+ List<Entity> list = new ArrayList<Entity>();
+ Entity e1 = new Entity();
+ e1.setId(1L);
+ e1.setSortValue(1L);
+ list.add(e1);
+ Entity e2 = new Entity();
+ e2.setId(2L);
+ e2.setSortValue(null);
+ list.add(e2);
+ Collections.sort(list, myComparator);
+
+看到这里的e2的排序值是null,在Comparator中如果要正常运行的话,就得判空之类的,这里有两点需要,一个是不想写这个MyComparator,然后也没那么好排除掉list里排序值,那么有什么办法能解决这种问题呢,应该说java的这方面真的是很强大
+
看一下nullsFirst的实现
+final static class NullComparator<T> implements Comparator<T>, Serializable {
+ private static final long serialVersionUID = -7569533591570686392L;
+ private final boolean nullFirst;
+ // if null, non-null Ts are considered equal
+ private final Comparator<T> real;
+
+ @SuppressWarnings("unchecked")
+ NullComparator(boolean nullFirst, Comparator<? super T> real) {
+ this.nullFirst = nullFirst;
+ this.real = (Comparator<T>) real;
+ }
+
+ @Override
+ public int compare(T a, T b) {
+ if (a == null) {
+ return (b == null) ? 0 : (nullFirst ? -1 : 1);
+ } else if (b == null) {
+ return nullFirst ? 1: -1;
+ } else {
+ return (real == null) ? 0 : real.compare(a, b);
+ }
+ }
+
+核心代码就是下面这段,其实就是帮我们把前面要做的事情做掉了,是不是挺方便的,小记一下哈
+]]>ArrayBlockingQueue,因为这个阻塞队列是使用了锁来控制阻塞,关于并发其实有一些通用的最佳实践,就是用锁,即使是 JDK 提供的锁,也是比较耗资源的,当然这是跟不加锁的对比,同样是锁,JDK 的实现还是性能比较优秀的。常见的阻塞队列中例如 ArrayBlockingQueue 和 LinkedBlockingQueue 都有锁的身影的存在,区别在于 ArrayBlockingQueue 是一把锁,后者是两把锁,不过重点不在几把锁,这里其实是两个问题,一个是所谓的 lock free, 对于一个单生产者的 disruptor 来说,因为写入是只有一个线程的,是可以不用加锁,多生产者的时候使用的是 cas 来获取对应的写入坑位,另一个是解决“伪共享”问题,后面可以详细点分析,先介绍下使用public class LongEvent {
+ private long value;
+
+ public void set(long value) {
+ this.value = value;
+ }
+
+ public long getValue() {
+ return value;
+ }
+
+ public void setValue(long value) {
+ this.value = value;
+ }
+}
+事件生产
+public class LongEventFactory implements EventFactory<LongEvent>
+{
+ public LongEvent newInstance()
+ {
+ return new LongEvent();
+ }
+}
+事件处理器
+public class LongEventHandler implements EventHandler<LongEvent> {
+
+ // event 事件,
+ // sequence 当前的序列
+ // 是否当前批次最后一个数据
+ public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
+ {
+ String str = String.format("long event : %s l:%s b:%s", event.getValue(), sequence, endOfBatch);
+ System.out.println(str);
+ }
+}
+
+主方法代码
+package disruptor;
+
+import com.lmax.disruptor.RingBuffer;
+import com.lmax.disruptor.dsl.Disruptor;
+import com.lmax.disruptor.util.DaemonThreadFactory;
+
+import java.nio.ByteBuffer;
+
+public class LongEventMain
+{
+ public static void main(String[] args) throws Exception
+ {
+ // 这个需要是 2 的幂次,这样在定位的时候只需要位移操作,也能减少各种计算操作
+ int bufferSize = 1024;
+
+ Disruptor<LongEvent> disruptor =
+ new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
+
+ // 类似于注册处理器
+ disruptor.handleEventsWith(new LongEventHandler());
+ // 或者直接用 lambda
+ disruptor.handleEventsWith((event, sequence, endOfBatch) ->
+ System.out.println("Event: " + event));
+ // 启动我们的 disruptor
+ disruptor.start();
+
+
+ RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
+ ByteBuffer bb = ByteBuffer.allocate(8);
+ for (long l = 0; true; l++)
+ {
+ bb.putLong(0, l);
+ // 生产事件
+ ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
+ Thread.sleep(1000);
+ }
+ }
+}
+运行下可以看到运行结果
这里其实就只是最简单的使用,生产者只有一个,然后也不是批量的。
<dubbo:registry address="zookeeper://127.0.0.1:2181" register="false" />
-就是只要 register="false" 就可以了,这样比如我们在开发环境想运行服务,但又不想让开发环境正常的请求调用到本地来,当然这不是唯一的方式,通过 dubbo 2.7 以上的 tag 路由也可以实现或者自行改造拉取和注册服务的逻辑,因为注册到注册中心的其实是一串带参数的 url,还是比较方便改造的。相反的就是只注册,不拉取
<dubbo:registry address="zookeeper://127.0.0.1:2181" subscribe="false" />
-这个使用场景就是如果我这个服务只作为 provider,没有任何调用其他的服务,其实就可以这么设置
-<dubbo:provider loadbalance="random" weight="50"/>
-首先这是在使用了随机的负载均衡策略的时候可以进行配置,并且是对于多个 provider 的情况下,这样其实也可以部分解决上面的只拉取不注册的问题,我把自己的权重调成 0 或者很低
-]]><dubbo:registry address="zookeeper://127.0.0.1:2181" register="false" />
+就是只要 register="false" 就可以了,这样比如我们在开发环境想运行服务,但又不想让开发环境正常的请求调用到本地来,当然这不是唯一的方式,通过 dubbo 2.7 以上的 tag 路由也可以实现或者自行改造拉取和注册服务的逻辑,因为注册到注册中心的其实是一串带参数的 url,还是比较方便改造的。相反的就是只注册,不拉取
<dubbo:registry address="zookeeper://127.0.0.1:2181" subscribe="false" />
+这个使用场景就是如果我这个服务只作为 provider,没有任何调用其他的服务,其实就可以这么设置
+<dubbo:provider loadbalance="random" weight="50"/>
+首先这是在使用了随机的负载均衡策略的时候可以进行配置,并且是对于多个 provider 的情况下,这样其实也可以部分解决上面的只拉取不注册的问题,我把自己的权重调成 0 或者很低
+]]>HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
- uint gc_count_before,
- bool* succeeded,
- GCCause::Cause gc_cause) {
- assert_heap_not_locked_and_not_at_safepoint();
- VM_G1CollectForAllocation op(word_size,
- gc_count_before,
- gc_cause,
- false, /* should_initiate_conc_mark */
- g1_policy()->max_pause_time_ms());
- VMThread::execute(&op);
-
- HeapWord* result = op.result();
- bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
- assert(result == NULL || ret_succeeded,
- "the result should be NULL if the VM did not succeed");
- *succeeded = ret_succeeded;
+ Headscale初体验以及踩坑记
+ /2023/01/22/Headscale%E5%88%9D%E4%BD%93%E9%AA%8C%E4%BB%A5%E5%8F%8A%E8%B8%A9%E5%9D%91%E8%AE%B0/
+ 最近或者说很久以前就想着能够把几个散装服务器以及家里的网络连起来,譬如一些remote desktop的访问,之前搞了下frp,因为家里电脑没怎么注意安全性就被搞了一下,所以还是想用相对更安全的方式,比如限定ip和端口进行访问,但是感觉ip也不固定就比较难搞,后来看到了 Tailscale 和 Headscale 的方式,就想着试试看,没想到一开始就踩了几个比较莫名其妙的坑。
可以按官方文档去搭建,也可以在网上找一些其他人搭建的教程。我碰到的主要是关于配置文件的问题
+第一个问题
Error initializing error="failed to read or create private key: failed to save private key to disk: open /etc/headscale/private.key: read-only file system"
+其实一开始看到这个我都有点懵了,咋回事呢,read-only file system一般有可能是文件系统出问题了,不可写入,需要重启或者修改挂载方式,被这个错误的错误日志给误导了,后面才知道是配置文件,在另一个教程中也有个类似的回复,一开始没注意,其实就是同一个问题。
默认的配置文件是这样的
+---
+# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
+#
+# - `/etc/headscale`
+# - `~/.headscale`
+# - current working directory
- assert_heap_not_locked();
- return result;
-}
-这里就是收集时需要停顿的,其中VMThread::execute(&op);是具体执行的,真正执行的是VM_G1CollectForAllocation::doit方法
-void VM_G1CollectForAllocation::doit() {
- G1CollectedHeap* g1h = G1CollectedHeap::heap();
- assert(!_should_initiate_conc_mark || g1h->should_do_concurrent_full_gc(_gc_cause),
- "only a GC locker, a System.gc(), stats update, whitebox, or a hum allocation induced GC should start a cycle");
+# The url clients will connect to.
+# Typically this will be a domain like:
+#
+# https://myheadscale.example.com:443
+#
+server_url: http://127.0.0.1:8080
- if (_word_size > 0) {
- // An allocation has been requested. So, try to do that first.
- _result = g1h->attempt_allocation_at_safepoint(_word_size,
- false /* expect_null_cur_alloc_region */);
- if (_result != NULL) {
- // If we can successfully allocate before we actually do the
- // pause then we will consider this pause successful.
- _pause_succeeded = true;
- return;
- }
- }
+# Address to listen to / bind to on the server
+#
+# For production:
+# listen_addr: 0.0.0.0:8080
+listen_addr: 127.0.0.1:8080
- GCCauseSetter x(g1h, _gc_cause);
- if (_should_initiate_conc_mark) {
- // It's safer to read old_marking_cycles_completed() here, given
- // that noone else will be updating it concurrently. Since we'll
- // only need it if we're initiating a marking cycle, no point in
- // setting it earlier.
- _old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();
+# Address to listen to /metrics, you may want
+# to keep this endpoint private to your internal
+# network
+#
+metrics_listen_addr: 127.0.0.1:9090
- // At this point we are supposed to start a concurrent cycle. We
- // will do so if one is not already in progress.
- bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);
+# Address to listen for gRPC.
+# gRPC is used for controlling a headscale server
+# remotely with the CLI
+# Note: Remote access _only_ works if you have
+# valid certificates.
+#
+# For production:
+# grpc_listen_addr: 0.0.0.0:50443
+grpc_listen_addr: 127.0.0.1:50443
- // The above routine returns true if we were able to force the
- // next GC pause to be an initial mark; it returns false if a
- // marking cycle is already in progress.
- //
- // If a marking cycle is already in progress just return and skip the
- // pause below - if the reason for requesting this initial mark pause
- // was due to a System.gc() then the requesting thread should block in
- // doit_epilogue() until the marking cycle is complete.
- //
- // If this initial mark pause was requested as part of a humongous
- // allocation then we know that the marking cycle must just have
- // been started by another thread (possibly also allocating a humongous
- // object) as there was no active marking cycle when the requesting
- // thread checked before calling collect() in
- // attempt_allocation_humongous(). Retrying the GC, in this case,
- // will cause the requesting thread to spin inside collect() until the
- // just started marking cycle is complete - which may be a while. So
- // we do NOT retry the GC.
- if (!res) {
- assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating");
- if (_gc_cause != GCCause::_g1_humongous_allocation) {
- _should_retry_gc = true;
- }
- return;
- }
- }
+# Allow the gRPC admin interface to run in INSECURE
+# mode. This is not recommended as the traffic will
+# be unencrypted. Only enable if you know what you
+# are doing.
+grpc_allow_insecure: false
- // Try a partial collection of some kind.
- _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
+# Private key used to encrypt the traffic between headscale
+# and Tailscale clients.
+# The private key file will be autogenerated if it's missing.
+#
+# For production:
+# /var/lib/headscale/private.key
+private_key_path: ./private.key
- if (_pause_succeeded) {
- if (_word_size > 0) {
- // An allocation had been requested. Do it, eventually trying a stronger
- // kind of GC.
- _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded);
- } else {
- bool should_upgrade_to_full = !g1h->should_do_concurrent_full_gc(_gc_cause) &&
- !g1h->has_regions_left_for_allocation();
- if (should_upgrade_to_full) {
- // There has been a request to perform a GC to free some space. We have no
- // information on how much memory has been asked for. In case there are
- // absolutely no regions left to allocate into, do a maximally compacting full GC.
- log_info(gc, ergo)("Attempting maximally compacting collection");
- _pause_succeeded = g1h->do_full_collection(false, /* explicit gc */
- true /* clear_all_soft_refs */);
- }
- }
- guarantee(_pause_succeeded, "Elevated collections during the safepoint must always succeed.");
- } else {
- assert(_result == NULL, "invariant");
- // The only reason for the pause to not be successful is that, the GC locker is
- // active (or has become active since the prologue was executed). In this case
- // we should retry the pause after waiting for the GC locker to become inactive.
- _should_retry_gc = true;
- }
-}
-这里可以看到核心的是G1CollectedHeap::do_collection_pause_at_safepoint这个方法,它带上了目标暂停时间的值
-G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {
- assert_at_safepoint_on_vm_thread();
- guarantee(!is_gc_active(), "collection is not reentrant");
+# The Noise section includes specific configuration for the
+# TS2021 Noise protocol
+noise:
+ # The Noise private key is used to encrypt the
+ # traffic between headscale and Tailscale clients when
+ # using the new Noise-based protocol. It must be different
+ # from the legacy private key.
+ #
+ # For production:
+ # private_key_path: /var/lib/headscale/noise_private.key
+ private_key_path: ./noise_private.key
- if (GCLocker::check_active_before_gc()) {
- return false;
- }
+# List of IP prefixes to allocate tailaddresses from.
+# Each prefix consists of either an IPv4 or IPv6 address,
+# and the associated prefix length, delimited by a slash.
+# While this looks like it can take arbitrary values, it
+# needs to be within IP ranges supported by the Tailscale
+# client.
+# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
+# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
+ip_prefixes:
+ - fd7a:115c:a1e0::/48
+ - 100.64.0.0/10
- _gc_timer_stw->register_gc_start();
+# DERP is a relay system that Tailscale uses when a direct
+# connection cannot be established.
+# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
+#
+# headscale needs a list of DERP servers that can be presented
+# to the clients.
+derp:
+ server:
+ # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
+ # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
+ enabled: false
- GCIdMark gc_id_mark;
- _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
+ # Region ID to use for the embedded DERP server.
+ # The local DERP prevails if the region ID collides with other region ID coming from
+ # the regular DERP config.
+ region_id: 999
- SvcGCMarker sgcm(SvcGCMarker::MINOR);
- ResourceMark rm;
+ # Region code and name are displayed in the Tailscale UI to identify a DERP region
+ region_code: "headscale"
+ region_name: "Headscale Embedded DERP"
- g1_policy()->note_gc_start();
+ # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
+ # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
+ #
+ # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
+ stun_listen_addr: "0.0.0.0:3478"
- wait_for_root_region_scanning();
+ # List of externally available DERP maps encoded in JSON
+ urls:
+ - https://controlplane.tailscale.com/derpmap/default
- print_heap_before_gc();
- print_heap_regions();
- trace_heap_before_gc(_gc_tracer_stw);
+ # Locally available DERP map files encoded in YAML
+ #
+ # This option is mostly interesting for people hosting
+ # their own DERP servers:
+ # https://tailscale.com/kb/1118/custom-derp-servers/
+ #
+ # paths:
+ # - /etc/headscale/derp-example.yaml
+ paths: []
- _verifier->verify_region_sets_optional();
- _verifier->verify_dirty_young_regions();
+ # If enabled, a worker will be set up to periodically
+ # refresh the given sources and update the derpmap
+ # will be set up.
+ auto_update_enabled: true
- // We should not be doing initial mark unless the conc mark thread is running
- if (!_cm_thread->should_terminate()) {
- // This call will decide whether this pause is an initial-mark
- // pause. If it is, in_initial_mark_gc() will return true
- // for the duration of this pause.
- g1_policy()->decide_on_conc_mark_initiation();
- }
+ # How often should we check for DERP updates?
+ update_frequency: 24h
- // We do not allow initial-mark to be piggy-backed on a mixed GC.
- assert(!collector_state()->in_initial_mark_gc() ||
- collector_state()->in_young_only_phase(), "sanity");
+# Disables the automatic check for headscale updates on startup
+disable_check_updates: false
- // We also do not allow mixed GCs during marking.
- assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
+# Time before an inactive ephemeral node is deleted?
+ephemeral_node_inactivity_timeout: 30m
- // Record whether this pause is an initial mark. When the current
- // thread has completed its logging output and it's safe to signal
- // the CM thread, the flag's value in the policy has been reset.
- bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
+# Period to check for node updates within the tailnet. A value too low will severely affect
+# CPU consumption of Headscale. A value too high (over 60s) will cause problems
+# for the nodes, as they won't get updates or keep alive messages frequently enough.
+# In case of doubts, do not touch the default 10s.
+node_update_check_interval: 10s
- // Inner scope for scope based logging, timers, and stats collection
- {
- EvacuationInfo evacuation_info;
+# SQLite config
+db_type: sqlite3
- if (collector_state()->in_initial_mark_gc()) {
- // We are about to start a marking cycle, so we increment the
- // full collection counter.
- increment_old_marking_cycles_started();
- _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
- }
+# For production:
+# db_path: /var/lib/headscale/db.sqlite
+db_path: ./db.sqlite
- _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
+# # Postgres config
+# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
+# db_type: postgres
+# db_host: localhost
+# db_port: 5432
+# db_name: headscale
+# db_user: foo
+# db_pass: bar
- GCTraceCPUTime tcpu;
+# If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
+# in the 'db_ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
+# db_ssl: false
- G1HeapVerifier::G1VerifyType verify_type;
- FormatBuffer<> gc_string("Pause Young ");
- if (collector_state()->in_initial_mark_gc()) {
- gc_string.append("(Concurrent Start)");
- verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
- } else if (collector_state()->in_young_only_phase()) {
- if (collector_state()->in_young_gc_before_mixed()) {
- gc_string.append("(Prepare Mixed)");
- } else {
- gc_string.append("(Normal)");
- }
- verify_type = G1HeapVerifier::G1VerifyYoungNormal;
- } else {
- gc_string.append("(Mixed)");
- verify_type = G1HeapVerifier::G1VerifyMixed;
- }
- GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
-
- uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
- workers()->active_workers(),
- Threads::number_of_non_daemon_threads());
- active_workers = workers()->update_active_workers(active_workers);
- log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
-
- TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
- TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
- collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
-
- G1HeapTransition heap_transition(this);
- size_t heap_used_bytes_before_gc = used();
-
- // Don't dynamically change the number of GC threads this early. A value of
- // 0 is used to indicate serial work. When parallel work is done,
- // it will be set.
-
- { // Call to jvmpi::post_class_unload_events must occur outside of active GC
- IsGCActiveMark x;
-
- gc_prologue(false);
-
- if (VerifyRememberedSets) {
- log_info(gc, verify)("[Verifying RemSets before GC]");
- VerifyRegionRemSetClosure v_cl;
- heap_region_iterate(&v_cl);
- }
-
- _verifier->verify_before_gc(verify_type);
-
- _verifier->check_bitmaps("GC Start");
-
-#if COMPILER2_OR_JVMCI
- DerivedPointerTable::clear();
-#endif
-
- // Please see comment in g1CollectedHeap.hpp and
- // G1CollectedHeap::ref_processing_init() to see how
- // reference processing currently works in G1.
-
- // Enable discovery in the STW reference processor
- _ref_processor_stw->enable_discovery();
+### TLS configuration
+#
+## Let's encrypt / ACME
+#
+# headscale supports automatically requesting and setting up
+# TLS for a domain with Let's Encrypt.
+#
+# URL to ACME directory
+acme_url: https://acme-v02.api.letsencrypt.org/directory
- {
- // We want to temporarily turn off discovery by the
- // CM ref processor, if necessary, and turn it back on
- // on again later if we do. Using a scoped
- // NoRefDiscovery object will do this.
- NoRefDiscovery no_cm_discovery(_ref_processor_cm);
+# Email to register with ACME provider
+acme_email: ""
- // Forget the current alloc region (we might even choose it to be part
- // of the collection set!).
- _allocator->release_mutator_alloc_region();
+# Domain name to request a TLS certificate for:
+tls_letsencrypt_hostname: ""
- // This timing is only used by the ergonomics to handle our pause target.
- // It is unclear why this should not include the full pause. We will
- // investigate this in CR 7178365.
- //
- // Preserving the old comment here if that helps the investigation:
- //
- // The elapsed time induced by the start time below deliberately elides
- // the possible verification above.
- double sample_start_time_sec = os::elapsedTime();
+# Path to store certificates and metadata needed by
+# letsencrypt
+# For production:
+# tls_letsencrypt_cache_dir: /var/lib/headscale/cache
+tls_letsencrypt_cache_dir: ./cache
- g1_policy()->record_collection_pause_start(sample_start_time_sec);
+# Type of ACME challenge to use, currently supported types:
+# HTTP-01 or TLS-ALPN-01
+# See [docs/tls.md](docs/tls.md) for more information
+tls_letsencrypt_challenge_type: HTTP-01
+# When HTTP-01 challenge is chosen, letsencrypt must set up a
+# verification endpoint, and it will be listening on:
+# :http = port 80
+tls_letsencrypt_listen: ":http"
- if (collector_state()->in_initial_mark_gc()) {
- concurrent_mark()->pre_initial_mark();
- }
+## Use already defined certificates:
+tls_cert_path: ""
+tls_key_path: ""
- g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
+log:
+ # Output formatting for logs: text or json
+ format: text
+ level: info
- evacuation_info.set_collectionset_regions(collection_set()->region_length());
+# Path to a file containg ACL policies.
+# ACLs can be defined as YAML or HUJSON.
+# https://tailscale.com/kb/1018/acls/
+acl_policy_path: ""
- // Make sure the remembered sets are up to date. This needs to be
- // done before register_humongous_regions_with_cset(), because the
- // remembered sets are used there to choose eager reclaim candidates.
- // If the remembered sets are not up to date we might miss some
- // entries that need to be handled.
- g1_rem_set()->cleanupHRRS();
+## DNS
+#
+# headscale supports Tailscale's DNS configuration and MagicDNS.
+# Please have a look to their KB to better understand the concepts:
+#
+# - https://tailscale.com/kb/1054/dns/
+# - https://tailscale.com/kb/1081/magicdns/
+# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
+#
+dns_config:
+ # Whether to prefer using Headscale provided DNS or use local.
+ override_local_dns: true
- register_humongous_regions_with_cset();
+ # List of DNS servers to expose to clients.
+ nameservers:
+ - 1.1.1.1
- assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
+ # NextDNS (see https://tailscale.com/kb/1218/nextdns/).
+ # "abc123" is example NextDNS ID, replace with yours.
+ #
+ # With metadata sharing:
+ # nameservers:
+ # - https://dns.nextdns.io/abc123
+ #
+ # Without metadata sharing:
+ # nameservers:
+ # - 2a07:a8c0::ab:c123
+ # - 2a07:a8c1::ab:c123
- // We call this after finalize_cset() to
- // ensure that the CSet has been finalized.
- _cm->verify_no_cset_oops();
+ # Split DNS (see https://tailscale.com/kb/1054/dns/),
+ # list of search domains and the DNS to query for each one.
+ #
+ # restricted_nameservers:
+ # foo.bar.com:
+ # - 1.1.1.1
+ # darp.headscale.net:
+ # - 1.1.1.1
+ # - 8.8.8.8
- if (_hr_printer.is_active()) {
- G1PrintCollectionSetClosure cl(&_hr_printer);
- _collection_set.iterate(&cl);
- }
+ # Search domains to inject.
+ domains: []
- // Initialize the GC alloc regions.
- _allocator->init_gc_alloc_regions(evacuation_info);
+ # Extra DNS records
+ # so far only A-records are supported (on the tailscale side)
+ # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
+ # extra_records:
+ # - name: "grafana.myvpn.example.com"
+ # type: "A"
+ # value: "100.64.0.3"
+ #
+ # # you can also put it in one line
+ # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
- G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
- pre_evacuate_collection_set();
+ # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
+ # Only works if there is at least a nameserver defined.
+ magic_dns: true
- // Actually do the work...
- evacuate_collection_set(&per_thread_states);
+ # Defines the base domain to create the hostnames for MagicDNS.
+ # `base_domain` must be a FQDNs, without the trailing dot.
+ # The FQDN of the hosts will be
+ # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_).
+ base_domain: example.com
- post_evacuate_collection_set(evacuation_info, &per_thread_states);
+# Unix socket used for the CLI to connect without authentication
+# Note: for production you will want to set this to something like:
+# unix_socket: /var/run/headscale.sock
+unix_socket: ./headscale.sock
+unix_socket_permission: "0770"
+#
+# headscale supports experimental OpenID connect support,
+# it is still being tested and might have some bugs, please
+# help us test it.
+# OpenID Connect
+# oidc:
+# only_start_if_oidc_is_available: true
+# issuer: "https://your-oidc.issuer.com/path"
+# client_id: "your-oidc-client-id"
+# client_secret: "your-oidc-client-secret"
+# # Alternatively, set `client_secret_path` to read the secret from the file.
+# # It resolves environment variables, making integration to systemd's
+# # `LoadCredential` straightforward:
+# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
+# # client_secret and client_secret_path are mutually exclusive.
+#
+# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
+# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
+#
+# scope: ["openid", "profile", "email", "custom"]
+# extra_params:
+# domain_hint: example.com
+#
+# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
+# authentication request will be rejected.
+#
+# allowed_domains:
+# - example.com
+# Groups from keycloak have a leading '/'
+# allowed_groups:
+# - /headscale
+# allowed_users:
+# - alice@example.com
+#
+# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
+# This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
+# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
+# user: `first-name.last-name.example.com`
+#
+# strip_email_domain: true
- const size_t* surviving_young_words = per_thread_states.surviving_young_words();
- free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
+# Logtail configuration
+# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
+# to instruct tailscale nodes to log their activity to a remote server.
+logtail:
+ # Enable logtail for this headscales clients.
+ # As there is currently no support for overriding the log server in headscale, this is
+ # disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
+ enabled: false
- eagerly_reclaim_humongous_regions();
+# Enabling this option makes devices prefer a random port for WireGuard traffic over the
+# default static port 41641. This option is intended as a workaround for some buggy
+# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
+randomize_client_port: false
- record_obj_copy_mem_stats();
- _survivor_evac_stats.adjust_desired_plab_sz();
- _old_evac_stats.adjust_desired_plab_sz();
+问题就是出在几个文件路径的配置,默认都是当前目录,也就是headscale的可执行文件所在目录,需要按它配置说明中的生产配置进行修改
+# For production:
+# /var/lib/headscale/private.key
+private_key_path: /var/lib/headscale/private.key
+直接改成绝对路径就好了,还有两个文件路径
另一个也是个秘钥的路径问题
+noise:
+ # The Noise private key is used to encrypt the
+ # traffic between headscale and Tailscale clients when
+ # using the new Noise-based protocol. It must be different
+ # from the legacy private key.
+ #
+ # For production:
+ # private_key_path: /var/lib/headscale/noise_private.key
+ private_key_path: /var/lib/headscale/noise_private.key
+第二个问题
这个问题也是一种误导,
错误信息是
+Error initializing error="unable to open database file: out of memory (14)"
+这就是个文件,内存也完全没有被占满的迹象,原来也是文件路径的问题
+# For production:
+# db_path: /var/lib/headscale/db.sqlite
+db_path: /var/lib/headscale/db.sqlite
+都改成绝对路径就可以了,然后这里还有个就是要对/var/lib/headscale/和/etc/headscale/等路径赋予headscale用户权限,有时候对这类问题的排查真的蛮头疼,日志报错都不是真实的错误信息,开源项目对这些错误的提示真的也需要优化,后续的譬如mac也加入节点等后面再开篇讲
+]]>
+
+ headscale
+
+
+ headscale
+
+ Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.
+将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
+
++输入:l1 = [1,2,4], l2 = [1,3,4]
+
输出:[1,1,2,3,4,4]
++输入: l1 = [], l2 = []
+
输出: []
++输入: l1 = [], l2 = [0]
+
输出: [0]
这题是 Easy 的,看着也挺简单,两个链表进行合并,就是比较下大小,可能将就点的话最好就在两个链表中原地合并
+public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
+ // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了
+ if (l1 == null) {
+ return l2;
+ }
+ if (l2 == null) {
+ return l1;
+ }
+ // new 一个合并后的头结点
+ ListNode merged = new ListNode();
+ // 这个是当前节点
+ ListNode current = merged;
+ // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要
+ // 因为内部前两个if就是跳出条件
+ while (true) {
+ if (l1 == null) {
+ // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面
+ // 所以不能是直接current = l2,这样就是把后面的直接丢了
+ current.val = l2.val;
+ current.next = l2.next;
+ break;
+ }
+ if (l2 == null) {
+ current.val = l1.val;
+ current.next = l1.next;
+ break;
+ }
+ // 这里是两个链表都不为空的时候,就比较下大小
+ if (l1.val < l2.val) {
+ current.val = l1.val;
+ l1 = l1.next;
+ } else {
+ current.val = l2.val;
+ l2 = l2.next;
+ }
+ // 这里是new个新的,其实也可以放在循环头上
+ current.next = new ListNode();
+ current = current.next;
+ }
+ current = null;
+ // 返回这个头结点
+ return merged;
+ }
- double start = os::elapsedTime();
- start_new_collection_set();
- g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
+
Implement strStr().
Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.
What should we return when needle is an empty string? This is a great question to ask during an interview.
For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C’s strstr() and Java’s indexOf().
Example 1:
+Input: haystack = "hello", needle = "ll"
+Output: 2
+Example 2:
+Input: haystack = "aaaaa", needle = "bba"
+Output: -1
+Example 3:
+Input: haystack = "", needle = ""
+Output: 0
- if (evacuation_failed()) {
- set_used(recalculate_used());
- if (_archive_allocator != NULL) {
- _archive_allocator->clear_used();
- }
- for (uint i = 0; i < ParallelGCThreads; i++) {
- if (_evacuation_failed_info_array[i].has_failed()) {
- _gc_tracer_stw->report_evacuation_failed(_evacuation_failed_info_array[i]);
- }
- }
- } else {
- // The "used" of the the collection set have already been subtracted
- // when they were freed. Add in the bytes evacuated.
- increase_used(g1_policy()->bytes_copied_during_gc());
- }
+字符串比较其实是写代码里永恒的主题,底层的编译器等处理肯定需要字符串对比,像 kmp 算法也是很厉害
+public int strStr(String haystack, String needle) {
+ // 如果两个字符串都为空,返回 -1
+ if (haystack == null || needle == null) {
+ return -1;
+ }
+ // 如果 haystack 长度小于 needle 长度,返回 -1
+ if (haystack.length() < needle.length()) {
+ return -1;
+ }
+ // 如果 needle 为空字符串,返回 0
+ if (needle.equals("")) {
+ return 0;
+ }
+ // 如果两者相等,返回 0
+ if (haystack.equals(needle)) {
+ return 0;
+ }
+ int needleLength = needle.length();
+ int haystackLength = haystack.length();
+ for (int i = needleLength - 1; i <= haystackLength - 1; i++) {
+ // 比较 needle 最后一个字符,倒着比较稍微节省点时间
+ if (needle.charAt(needleLength - 1) == haystack.charAt(i)) {
+ // 如果needle 是 1 的话直接可以返回 i 作为位置了
+ if (needle.length() == 1) {
+ return i;
+ }
+ boolean flag = true;
+ // 原来比的是 needle 的最后一个位置,然后这边从倒数第二个位置开始
+ int j = needle.length() - 2;
+ for (; j >= 0; j--) {
+ // 这里的 i- (needleLength - j) + 1 ) 比较绕,其实是外循环的 i 表示当前 i 位置的字符跟 needle 最后一个字符
+ // 相同,j 在上面的循环中--,对应的 haystack 也要在 i 这个位置 -- ,对应的位置就是 i - (needleLength - j) + 1
+ if (needle.charAt(j) != haystack.charAt(i - (needleLength - j) + 1)) {
+ flag = false;
+ break;
+ }
+ }
+ // 循环完了之后,如果 flag 为 true 说明 从 i 开始倒着对比都相同,但是这里需要起始位置,就需要
+ // i - needleLength + 1
+ if (flag) {
+ return i - needleLength + 1;
+ }
+ }
+ }
+ // 这里表示未找到
+ return -1;
+ }]]>HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,
+ uint gc_count_before,
+ bool* succeeded,
+ GCCause::Cause gc_cause) {
+ assert_heap_not_locked_and_not_at_safepoint();
+ VM_G1CollectForAllocation op(word_size,
+ gc_count_before,
+ gc_cause,
+ false, /* should_initiate_conc_mark */
+ g1_policy()->max_pause_time_ms());
+ VMThread::execute(&op);
- if (collector_state()->in_initial_mark_gc()) {
- // We have to do this before we notify the CM threads that
- // they can start working to make sure that all the
- // appropriate initialization is done on the CM object.
- concurrent_mark()->post_initial_mark();
- // Note that we don't actually trigger the CM thread at
- // this point. We do that later when we're sure that
- // the current thread has completed its logging output.
- }
+ HeapWord* result = op.result();
+ bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();
+ assert(result == NULL || ret_succeeded,
+ "the result should be NULL if the VM did not succeed");
+ *succeeded = ret_succeeded;
- allocate_dummy_regions();
+ assert_heap_not_locked();
+ return result;
+}
+这里就是收集时需要停顿的,其中VMThread::execute(&op);是具体执行的,真正执行的是VM_G1CollectForAllocation::doit方法
void VM_G1CollectForAllocation::doit() {
+ G1CollectedHeap* g1h = G1CollectedHeap::heap();
+ assert(!_should_initiate_conc_mark || g1h->should_do_concurrent_full_gc(_gc_cause),
+ "only a GC locker, a System.gc(), stats update, whitebox, or a hum allocation induced GC should start a cycle");
- _allocator->init_mutator_alloc_region();
+ if (_word_size > 0) {
+ // An allocation has been requested. So, try to do that first.
+ _result = g1h->attempt_allocation_at_safepoint(_word_size,
+ false /* expect_null_cur_alloc_region */);
+ if (_result != NULL) {
+ // If we can successfully allocate before we actually do the
+ // pause then we will consider this pause successful.
+ _pause_succeeded = true;
+ return;
+ }
+ }
- {
- size_t expand_bytes = _heap_sizing_policy->expansion_amount();
- if (expand_bytes > 0) {
- size_t bytes_before = capacity();
- // No need for an ergo logging here,
- // expansion_amount() does this when it returns a value > 0.
- double expand_ms;
- if (!expand(expand_bytes, _workers, &expand_ms)) {
- // We failed to expand the heap. Cannot do anything about it.
- }
- g1_policy()->phase_times()->record_expand_heap_time(expand_ms);
- }
- }
-
- // We redo the verification but now wrt to the new CSet which
- // has just got initialized after the previous CSet was freed.
- _cm->verify_no_cset_oops();
-
- // This timing is only used by the ergonomics to handle our pause target.
- // It is unclear why this should not include the full pause. We will
- // investigate this in CR 7178365.
- double sample_end_time_sec = os::elapsedTime();
- double pause_time_ms = (sample_end_time_sec - sample_start_time_sec) * MILLIUNITS;
- size_t total_cards_scanned = g1_policy()->phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanRS, G1GCPhaseTimes::ScanRSScannedCards);
- g1_policy()->record_collection_pause_end(pause_time_ms, total_cards_scanned, heap_used_bytes_before_gc);
-
- evacuation_info.set_collectionset_used_before(collection_set()->bytes_used_before());
- evacuation_info.set_bytes_copied(g1_policy()->bytes_copied_during_gc());
-
- if (VerifyRememberedSets) {
- log_info(gc, verify)("[Verifying RemSets after GC]");
- VerifyRegionRemSetClosure v_cl;
- heap_region_iterate(&v_cl);
- }
-
- _verifier->verify_after_gc(verify_type);
- _verifier->check_bitmaps("GC End");
+ GCCauseSetter x(g1h, _gc_cause);
+ if (_should_initiate_conc_mark) {
+ // It's safer to read old_marking_cycles_completed() here, given
+ // that noone else will be updating it concurrently. Since we'll
+ // only need it if we're initiating a marking cycle, no point in
+ // setting it earlier.
+ _old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();
- assert(!_ref_processor_stw->discovery_enabled(), "Postcondition");
- _ref_processor_stw->verify_no_references_recorded();
+ // At this point we are supposed to start a concurrent cycle. We
+ // will do so if one is not already in progress.
+ bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);
- // CM reference discovery will be re-enabled if necessary.
+ // The above routine returns true if we were able to force the
+ // next GC pause to be an initial mark; it returns false if a
+ // marking cycle is already in progress.
+ //
+ // If a marking cycle is already in progress just return and skip the
+ // pause below - if the reason for requesting this initial mark pause
+ // was due to a System.gc() then the requesting thread should block in
+ // doit_epilogue() until the marking cycle is complete.
+ //
+ // If this initial mark pause was requested as part of a humongous
+ // allocation then we know that the marking cycle must just have
+ // been started by another thread (possibly also allocating a humongous
+ // object) as there was no active marking cycle when the requesting
+ // thread checked before calling collect() in
+ // attempt_allocation_humongous(). Retrying the GC, in this case,
+ // will cause the requesting thread to spin inside collect() until the
+ // just started marking cycle is complete - which may be a while. So
+ // we do NOT retry the GC.
+ if (!res) {
+ assert(_word_size == 0, "Concurrent Full GC/Humongous Object IM shouldn't be allocating");
+ if (_gc_cause != GCCause::_g1_humongous_allocation) {
+ _should_retry_gc = true;
}
+ return;
+ }
+ }
-#ifdef TRACESPINNING
- ParallelTaskTerminator::print_termination_counts();
-#endif
+ // Try a partial collection of some kind.
+ _pause_succeeded = g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);
- gc_epilogue(false);
+ if (_pause_succeeded) {
+ if (_word_size > 0) {
+ // An allocation had been requested. Do it, eventually trying a stronger
+ // kind of GC.
+ _result = g1h->satisfy_failed_allocation(_word_size, &_pause_succeeded);
+ } else {
+ bool should_upgrade_to_full = !g1h->should_do_concurrent_full_gc(_gc_cause) &&
+ !g1h->has_regions_left_for_allocation();
+ if (should_upgrade_to_full) {
+ // There has been a request to perform a GC to free some space. We have no
+ // information on how much memory has been asked for. In case there are
+ // absolutely no regions left to allocate into, do a maximally compacting full GC.
+ log_info(gc, ergo)("Attempting maximally compacting collection");
+ _pause_succeeded = g1h->do_full_collection(false, /* explicit gc */
+ true /* clear_all_soft_refs */);
+ }
}
+ guarantee(_pause_succeeded, "Elevated collections during the safepoint must always succeed.");
+ } else {
+ assert(_result == NULL, "invariant");
+ // The only reason for the pause to not be successful is that, the GC locker is
+ // active (or has become active since the prologue was executed). In this case
+ // we should retry the pause after waiting for the GC locker to become inactive.
+ _should_retry_gc = true;
+ }
+}
+这里可以看到核心的是G1CollectedHeap::do_collection_pause_at_safepoint这个方法,它带上了目标暂停时间的值
G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {
+ assert_at_safepoint_on_vm_thread();
+ guarantee(!is_gc_active(), "collection is not reentrant");
- // Print the remainder of the GC log output.
- if (evacuation_failed()) {
- log_info(gc)("To-space exhausted");
- }
+ if (GCLocker::check_active_before_gc()) {
+ return false;
+ }
- g1_policy()->print_phases();
- heap_transition.print();
+ _gc_timer_stw->register_gc_start();
- // It is not yet to safe to tell the concurrent mark to
- // start as we have some optional output below. We don't want the
- // output from the concurrent mark thread interfering with this
- // logging output either.
+ GCIdMark gc_id_mark;
+ _gc_tracer_stw->report_gc_start(gc_cause(), _gc_timer_stw->gc_start());
- _hrm.verify_optional();
- _verifier->verify_region_sets_optional();
+ SvcGCMarker sgcm(SvcGCMarker::MINOR);
+ ResourceMark rm;
- TASKQUEUE_STATS_ONLY(print_taskqueue_stats());
- TASKQUEUE_STATS_ONLY(reset_taskqueue_stats());
+ g1_policy()->note_gc_start();
- print_heap_after_gc();
- print_heap_regions();
- trace_heap_after_gc(_gc_tracer_stw);
+ wait_for_root_region_scanning();
- // We must call G1MonitoringSupport::update_sizes() in the same scoping level
- // as an active TraceMemoryManagerStats object (i.e. before the destructor for the
- // TraceMemoryManagerStats is called) so that the G1 memory pools are updated
- // before any GC notifications are raised.
- g1mm()->update_sizes();
+ print_heap_before_gc();
+ print_heap_regions();
+ trace_heap_before_gc(_gc_tracer_stw);
- _gc_tracer_stw->report_evacuation_info(&evacuation_info);
- _gc_tracer_stw->report_tenuring_threshold(_g1_policy->tenuring_threshold());
- _gc_timer_stw->register_gc_end();
- _gc_tracer_stw->report_gc_end(_gc_timer_stw->gc_end(), _gc_timer_stw->time_partitions());
- }
- // It should now be safe to tell the concurrent mark thread to start
- // without its logging output interfering with the logging output
- // that came from the pause.
+ _verifier->verify_region_sets_optional();
+ _verifier->verify_dirty_young_regions();
- if (should_start_conc_mark) {
- // CAUTION: after the doConcurrentMark() call below,
- // the concurrent marking thread(s) could be running
- // concurrently with us. Make sure that anything after
- // this point does not assume that we are the only GC thread
- // running. Note: of course, the actual marking work will
- // not start until the safepoint itself is released in
- // SuspendibleThreadSet::desynchronize().
- do_concurrent_mark();
+ // We should not be doing initial mark unless the conc mark thread is running
+ if (!_cm_thread->should_terminate()) {
+ // This call will decide whether this pause is an initial-mark
+ // pause. If it is, in_initial_mark_gc() will return true
+ // for the duration of this pause.
+ g1_policy()->decide_on_conc_mark_initiation();
}
- return true;
-}
-往下走就是这一步G1Policy::finalize_collection_set,去处理新生代和老年代
void G1Policy::finalize_collection_set(double target_pause_time_ms, G1SurvivorRegions* survivor) {
- double time_remaining_ms = _collection_set->finalize_young_part(target_pause_time_ms, survivor);
- _collection_set->finalize_old_part(time_remaining_ms);
-}
-这里分别调用了两个方法,可以看到剩余时间是往下传的,来看一下具体的方法
-double G1CollectionSet::finalize_young_part(double target_pause_time_ms, G1SurvivorRegions* survivors) {
- double young_start_time_sec = os::elapsedTime();
+ // We do not allow initial-mark to be piggy-backed on a mixed GC.
+ assert(!collector_state()->in_initial_mark_gc() ||
+ collector_state()->in_young_only_phase(), "sanity");
- finalize_incremental_building();
+ // We also do not allow mixed GCs during marking.
+ assert(!collector_state()->mark_or_rebuild_in_progress() || collector_state()->in_young_only_phase(), "sanity");
- guarantee(target_pause_time_ms > 0.0,
- "target_pause_time_ms = %1.6lf should be positive", target_pause_time_ms);
+ // Record whether this pause is an initial mark. When the current
+ // thread has completed its logging output and it's safe to signal
+ // the CM thread, the flag's value in the policy has been reset.
+ bool should_start_conc_mark = collector_state()->in_initial_mark_gc();
- size_t pending_cards = _policy->pending_cards();
- double base_time_ms = _policy->predict_base_elapsed_time_ms(pending_cards);
- double time_remaining_ms = MAX2(target_pause_time_ms - base_time_ms, 0.0);
+ // Inner scope for scope based logging, timers, and stats collection
+ {
+ EvacuationInfo evacuation_info;
- log_trace(gc, ergo, cset)("Start choosing CSet. pending cards: " SIZE_FORMAT " predicted base time: %1.2fms remaining time: %1.2fms target pause time: %1.2fms",
- pending_cards, base_time_ms, time_remaining_ms, target_pause_time_ms);
+ if (collector_state()->in_initial_mark_gc()) {
+ // We are about to start a marking cycle, so we increment the
+ // full collection counter.
+ increment_old_marking_cycles_started();
+ _cm->gc_tracer_cm()->set_gc_cause(gc_cause());
+ }
- // The young list is laid with the survivor regions from the previous
- // pause are appended to the RHS of the young list, i.e.
- // [Newly Young Regions ++ Survivors from last pause].
+ _gc_tracer_stw->report_yc_type(collector_state()->yc_type());
- uint survivor_region_length = survivors->length();
- uint eden_region_length = _g1h->eden_regions_count();
- init_region_lengths(eden_region_length, survivor_region_length);
+ GCTraceCPUTime tcpu;
- verify_young_cset_indices();
+ G1HeapVerifier::G1VerifyType verify_type;
+ FormatBuffer<> gc_string("Pause Young ");
+ if (collector_state()->in_initial_mark_gc()) {
+ gc_string.append("(Concurrent Start)");
+ verify_type = G1HeapVerifier::G1VerifyConcurrentStart;
+ } else if (collector_state()->in_young_only_phase()) {
+ if (collector_state()->in_young_gc_before_mixed()) {
+ gc_string.append("(Prepare Mixed)");
+ } else {
+ gc_string.append("(Normal)");
+ }
+ verify_type = G1HeapVerifier::G1VerifyYoungNormal;
+ } else {
+ gc_string.append("(Mixed)");
+ verify_type = G1HeapVerifier::G1VerifyMixed;
+ }
+ GCTraceTime(Info, gc) tm(gc_string, NULL, gc_cause(), true);
- // Clear the fields that point to the survivor list - they are all young now.
- survivors->convert_to_eden();
+ uint active_workers = AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),
+ workers()->active_workers(),
+ Threads::number_of_non_daemon_threads());
+ active_workers = workers()->update_active_workers(active_workers);
+ log_info(gc,task)("Using %u workers of %u for evacuation", active_workers, workers()->total_workers());
- _bytes_used_before = _inc_bytes_used_before;
- time_remaining_ms = MAX2(time_remaining_ms - _inc_predicted_elapsed_time_ms, 0.0);
+ TraceCollectorStats tcs(g1mm()->incremental_collection_counters());
+ TraceMemoryManagerStats tms(&_memory_manager, gc_cause(),
+ collector_state()->yc_type() == Mixed /* allMemoryPoolsAffected */);
- log_trace(gc, ergo, cset)("Add young regions to CSet. eden: %u regions, survivors: %u regions, predicted young region time: %1.2fms, target pause time: %1.2fms",
- eden_region_length, survivor_region_length, _inc_predicted_elapsed_time_ms, target_pause_time_ms);
+ G1HeapTransition heap_transition(this);
+ size_t heap_used_bytes_before_gc = used();
- // The number of recorded young regions is the incremental
- // collection set's current size
- set_recorded_rs_lengths(_inc_recorded_rs_lengths);
+ // Don't dynamically change the number of GC threads this early. A value of
+ // 0 is used to indicate serial work. When parallel work is done,
+ // it will be set.
- double young_end_time_sec = os::elapsedTime();
- phase_times()->record_young_cset_choice_time_ms((young_end_time_sec - young_start_time_sec) * 1000.0);
+ { // Call to jvmpi::post_class_unload_events must occur outside of active GC
+ IsGCActiveMark x;
- return time_remaining_ms;
-}
-下面是老年代的部分
-void G1CollectionSet::finalize_old_part(double time_remaining_ms) {
- double non_young_start_time_sec = os::elapsedTime();
- double predicted_old_time_ms = 0.0;
+ gc_prologue(false);
- if (collector_state()->in_mixed_phase()) {
- cset_chooser()->verify();
- const uint min_old_cset_length = _policy->calc_min_old_cset_length();
- const uint max_old_cset_length = _policy->calc_max_old_cset_length();
+ if (VerifyRememberedSets) {
+ log_info(gc, verify)("[Verifying RemSets before GC]");
+ VerifyRegionRemSetClosure v_cl;
+ heap_region_iterate(&v_cl);
+ }
- uint expensive_region_num = 0;
- bool check_time_remaining = _policy->adaptive_young_list_length();
+ _verifier->verify_before_gc(verify_type);
- HeapRegion* hr = cset_chooser()->peek();
- while (hr != NULL) {
- if (old_region_length() >= max_old_cset_length) {
- // Added maximum number of old regions to the CSet.
- log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached max). old %u regions, max %u regions",
- old_region_length(), max_old_cset_length);
- break;
- }
+ _verifier->check_bitmaps("GC Start");
- // Stop adding regions if the remaining reclaimable space is
- // not above G1HeapWastePercent.
- size_t reclaimable_bytes = cset_chooser()->remaining_reclaimable_bytes();
- double reclaimable_percent = _policy->reclaimable_bytes_percent(reclaimable_bytes);
- double threshold = (double) G1HeapWastePercent;
- if (reclaimable_percent <= threshold) {
- // We've added enough old regions that the amount of uncollected
- // reclaimable space is at or below the waste threshold. Stop
- // adding old regions to the CSet.
- log_debug(gc, ergo, cset)("Finish adding old regions to CSet (reclaimable percentage not over threshold). "
- "old %u regions, max %u regions, reclaimable: " SIZE_FORMAT "B (%1.2f%%) threshold: " UINTX_FORMAT "%%",
- old_region_length(), max_old_cset_length, reclaimable_bytes, reclaimable_percent, G1HeapWastePercent);
- break;
- }
+#if COMPILER2_OR_JVMCI
+ DerivedPointerTable::clear();
+#endif
- double predicted_time_ms = predict_region_elapsed_time_ms(hr);
- if (check_time_remaining) {
- if (predicted_time_ms > time_remaining_ms) {
- // Too expensive for the current CSet.
+ // Please see comment in g1CollectedHeap.hpp and
+ // G1CollectedHeap::ref_processing_init() to see how
+ // reference processing currently works in G1.
- if (old_region_length() >= min_old_cset_length) {
- // We have added the minimum number of old regions to the CSet,
- // we are done with this CSet.
- log_debug(gc, ergo, cset)("Finish adding old regions to CSet (predicted time is too high). "
- "predicted time: %1.2fms, remaining time: %1.2fms old %u regions, min %u regions",
- predicted_time_ms, time_remaining_ms, old_region_length(), min_old_cset_length);
- break;
+ // Enable discovery in the STW reference processor
+ _ref_processor_stw->enable_discovery();
+
+ {
+ // We want to temporarily turn off discovery by the
+ // CM ref processor, if necessary, and turn it back on
+ // on again later if we do. Using a scoped
+ // NoRefDiscovery object will do this.
+ NoRefDiscovery no_cm_discovery(_ref_processor_cm);
+
+ // Forget the current alloc region (we might even choose it to be part
+ // of the collection set!).
+ _allocator->release_mutator_alloc_region();
+
+ // This timing is only used by the ergonomics to handle our pause target.
+ // It is unclear why this should not include the full pause. We will
+ // investigate this in CR 7178365.
+ //
+ // Preserving the old comment here if that helps the investigation:
+ //
+ // The elapsed time induced by the start time below deliberately elides
+ // the possible verification above.
+ double sample_start_time_sec = os::elapsedTime();
+
+ g1_policy()->record_collection_pause_start(sample_start_time_sec);
+
+ if (collector_state()->in_initial_mark_gc()) {
+ concurrent_mark()->pre_initial_mark();
+ }
+
+ g1_policy()->finalize_collection_set(target_pause_time_ms, &_survivor);
+
+ evacuation_info.set_collectionset_regions(collection_set()->region_length());
+
+ // Make sure the remembered sets are up to date. This needs to be
+ // done before register_humongous_regions_with_cset(), because the
+ // remembered sets are used there to choose eager reclaim candidates.
+ // If the remembered sets are not up to date we might miss some
+ // entries that need to be handled.
+ g1_rem_set()->cleanupHRRS();
+
+ register_humongous_regions_with_cset();
+
+ assert(_verifier->check_cset_fast_test(), "Inconsistency in the InCSetState table.");
+
+ // We call this after finalize_cset() to
+ // ensure that the CSet has been finalized.
+ _cm->verify_no_cset_oops();
+
+ if (_hr_printer.is_active()) {
+ G1PrintCollectionSetClosure cl(&_hr_printer);
+ _collection_set.iterate(&cl);
+ }
+
+ // Initialize the GC alloc regions.
+ _allocator->init_gc_alloc_regions(evacuation_info);
+
+ G1ParScanThreadStateSet per_thread_states(this, workers()->active_workers(), collection_set()->young_region_length());
+ pre_evacuate_collection_set();
+
+ // Actually do the work...
+ evacuate_collection_set(&per_thread_states);
+
+ post_evacuate_collection_set(evacuation_info, &per_thread_states);
+
+ const size_t* surviving_young_words = per_thread_states.surviving_young_words();
+ free_collection_set(&_collection_set, evacuation_info, surviving_young_words);
+
+ eagerly_reclaim_humongous_regions();
+
+ record_obj_copy_mem_stats();
+ _survivor_evac_stats.adjust_desired_plab_sz();
+ _old_evac_stats.adjust_desired_plab_sz();
+
+ double start = os::elapsedTime();
+ start_new_collection_set();
+ g1_policy()->phase_times()->record_start_new_cset_time_ms((os::elapsedTime() - start) * 1000.0);
+
+ if (evacuation_failed()) {
+ set_used(recalculate_used());
+ if (_archive_allocator != NULL) {
+ _archive_allocator->clear_used();
+ }
+ for (uint i = 0; i < ParallelGCThreads; i++) {
+ if (_evacuation_failed_info_array[i].has_failed()) {
+ _gc_tracer_stw->report_evacuation_failed(_evacuation_failed_info_array[i]);
+ }
}
+ } else {
+ // The "used" of the the collection set have already been subtracted
+ // when they were freed. Add in the bytes evacuated.
+ increase_used(g1_policy()->bytes_copied_during_gc());
+ }
- // We'll add it anyway given that we haven't reached the
- // minimum number of old regions.
- expensive_region_num += 1;
+ if (collector_state()->in_initial_mark_gc()) {
+ // We have to do this before we notify the CM threads that
+ // they can start working to make sure that all the
+ // appropriate initialization is done on the CM object.
+ concurrent_mark()->post_initial_mark();
+ // Note that we don't actually trigger the CM thread at
+ // this point. We do that later when we're sure that
+ // the current thread has completed its logging output.
}
- } else {
- if (old_region_length() >= min_old_cset_length) {
- // In the non-auto-tuning case, we'll finish adding regions
- // to the CSet if we reach the minimum.
- log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached min). old %u regions, min %u regions",
- old_region_length(), min_old_cset_length);
- break;
+ allocate_dummy_regions();
+
+ _allocator->init_mutator_alloc_region();
+
+ {
+ size_t expand_bytes = _heap_sizing_policy->expansion_amount();
+ if (expand_bytes > 0) {
+ size_t bytes_before = capacity();
+ // No need for an ergo logging here,
+ // expansion_amount() does this when it returns a value > 0.
+ double expand_ms;
+ if (!expand(expand_bytes, _workers, &expand_ms)) {
+ // We failed to expand the heap. Cannot do anything about it.
+ }
+ g1_policy()->phase_times()->record_expand_heap_time(expand_ms);
+ }
+ }
+
+ // We redo the verification but now wrt to the new CSet which
+ // has just got initialized after the previous CSet was freed.
+ _cm->verify_no_cset_oops();
+
+ // This timing is only used by the ergonomics to handle our pause target.
+ // It is unclear why this should not include the full pause. We will
+ // investigate this in CR 7178365.
+ double sample_end_time_sec = os::elapsedTime();
+ double pause_time_ms = (sample_end_time_sec - sample_start_time_sec) * MILLIUNITS;
+ size_t total_cards_scanned = g1_policy()->phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanRS, G1GCPhaseTimes::ScanRSScannedCards);
+ g1_policy()->record_collection_pause_end(pause_time_ms, total_cards_scanned, heap_used_bytes_before_gc);
+
+ evacuation_info.set_collectionset_used_before(collection_set()->bytes_used_before());
+ evacuation_info.set_bytes_copied(g1_policy()->bytes_copied_during_gc());
+
+ if (VerifyRememberedSets) {
+ log_info(gc, verify)("[Verifying RemSets after GC]");
+ VerifyRegionRemSetClosure v_cl;
+ heap_region_iterate(&v_cl);
}
+
+ _verifier->verify_after_gc(verify_type);
+ _verifier->check_bitmaps("GC End");
+
+ assert(!_ref_processor_stw->discovery_enabled(), "Postcondition");
+ _ref_processor_stw->verify_no_references_recorded();
+
+ // CM reference discovery will be re-enabled if necessary.
}
- // We will add this region to the CSet.
- time_remaining_ms = MAX2(time_remaining_ms - predicted_time_ms, 0.0);
- predicted_old_time_ms += predicted_time_ms;
- cset_chooser()->pop(); // already have region via peek()
- _g1h->old_set_remove(hr);
- add_old_region(hr);
+#ifdef TRACESPINNING
+ ParallelTaskTerminator::print_termination_counts();
+#endif
- hr = cset_chooser()->peek();
- }
- if (hr == NULL) {
- log_debug(gc, ergo, cset)("Finish adding old regions to CSet (candidate old regions not available)");
+ gc_epilogue(false);
}
- if (expensive_region_num > 0) {
- // We print the information once here at the end, predicated on
- // whether we added any apparently expensive regions or not, to
- // avoid generating output per region.
- log_debug(gc, ergo, cset)("Added expensive regions to CSet (old CSet region num not reached min)."
- "old: %u regions, expensive: %u regions, min: %u regions, remaining time: %1.2fms",
- old_region_length(), expensive_region_num, min_old_cset_length, time_remaining_ms);
+ // Print the remainder of the GC log output.
+ if (evacuation_failed()) {
+ log_info(gc)("To-space exhausted");
}
- cset_chooser()->verify();
- }
+ g1_policy()->print_phases();
+ heap_transition.print();
- stop_incremental_building();
+ // It is not yet to safe to tell the concurrent mark to
+ // start as we have some optional output below. We don't want the
+ // output from the concurrent mark thread interfering with this
+ // logging output either.
- log_debug(gc, ergo, cset)("Finish choosing CSet. old: %u regions, predicted old region time: %1.2fms, time remaining: %1.2f",
- old_region_length(), predicted_old_time_ms, time_remaining_ms);
+ _hrm.verify_optional();
+ _verifier->verify_region_sets_optional();
- double non_young_end_time_sec = os::elapsedTime();
- phase_times()->record_non_young_cset_choice_time_ms((non_young_end_time_sec - non_young_start_time_sec) * 1000.0);
+ TASKQUEUE_STATS_ONLY(print_taskqueue_stats());
+ TASKQUEUE_STATS_ONLY(reset_taskqueue_stats());
- QuickSort::sort(_collection_set_regions, _collection_set_cur_length, compare_region_idx, true);
-}
-上面第三行是个判断,当前是否是 mixed 回收阶段,如果不是的话其实是没有老年代什么事的,所以可以看到代码基本是从这个 if 判断if (collector_state()->in_mixed_phase()) {开始往下走的
先写到这,偏向于做笔记用,有错轻拍
往下走就是这一步G1Policy::finalize_collection_set,去处理新生代和老年代
void G1Policy::finalize_collection_set(double target_pause_time_ms, G1SurvivorRegions* survivor) {
+ double time_remaining_ms = _collection_set->finalize_young_part(target_pause_time_ms, survivor);
+ _collection_set->finalize_old_part(time_remaining_ms);
+}
+这里分别调用了两个方法,可以看到剩余时间是往下传的,来看一下具体的方法
+double G1CollectionSet::finalize_young_part(double target_pause_time_ms, G1SurvivorRegions* survivors) {
+ double young_start_time_sec = os::elapsedTime();
+
+ finalize_incremental_building();
+
+ guarantee(target_pause_time_ms > 0.0,
+ "target_pause_time_ms = %1.6lf should be positive", target_pause_time_ms);
+
+ size_t pending_cards = _policy->pending_cards();
+ double base_time_ms = _policy->predict_base_elapsed_time_ms(pending_cards);
+ double time_remaining_ms = MAX2(target_pause_time_ms - base_time_ms, 0.0);
+
+ log_trace(gc, ergo, cset)("Start choosing CSet. pending cards: " SIZE_FORMAT " predicted base time: %1.2fms remaining time: %1.2fms target pause time: %1.2fms",
+ pending_cards, base_time_ms, time_remaining_ms, target_pause_time_ms);
+
+ // The young list is laid with the survivor regions from the previous
+ // pause are appended to the RHS of the young list, i.e.
+ // [Newly Young Regions ++ Survivors from last pause].
+
+ uint survivor_region_length = survivors->length();
+ uint eden_region_length = _g1h->eden_regions_count();
+ init_region_lengths(eden_region_length, survivor_region_length);
+
+ verify_young_cset_indices();
+
+ // Clear the fields that point to the survivor list - they are all young now.
+ survivors->convert_to_eden();
+
+ _bytes_used_before = _inc_bytes_used_before;
+ time_remaining_ms = MAX2(time_remaining_ms - _inc_predicted_elapsed_time_ms, 0.0);
+
+ log_trace(gc, ergo, cset)("Add young regions to CSet. eden: %u regions, survivors: %u regions, predicted young region time: %1.2fms, target pause time: %1.2fms",
+ eden_region_length, survivor_region_length, _inc_predicted_elapsed_time_ms, target_pause_time_ms);
+
+ // The number of recorded young regions is the incremental
+ // collection set's current size
+ set_recorded_rs_lengths(_inc_recorded_rs_lengths);
+
+ double young_end_time_sec = os::elapsedTime();
+ phase_times()->record_young_cset_choice_time_ms((young_end_time_sec - young_start_time_sec) * 1000.0);
+
+ return time_remaining_ms;
+}
+下面是老年代的部分
+void G1CollectionSet::finalize_old_part(double time_remaining_ms) {
+ double non_young_start_time_sec = os::elapsedTime();
+ double predicted_old_time_ms = 0.0;
+
+ if (collector_state()->in_mixed_phase()) {
+ cset_chooser()->verify();
+ const uint min_old_cset_length = _policy->calc_min_old_cset_length();
+ const uint max_old_cset_length = _policy->calc_max_old_cset_length();
+
+ uint expensive_region_num = 0;
+ bool check_time_remaining = _policy->adaptive_young_list_length();
+
+ HeapRegion* hr = cset_chooser()->peek();
+ while (hr != NULL) {
+ if (old_region_length() >= max_old_cset_length) {
+ // Added maximum number of old regions to the CSet.
+ log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached max). old %u regions, max %u regions",
+ old_region_length(), max_old_cset_length);
+ break;
+ }
+
+ // Stop adding regions if the remaining reclaimable space is
+ // not above G1HeapWastePercent.
+ size_t reclaimable_bytes = cset_chooser()->remaining_reclaimable_bytes();
+ double reclaimable_percent = _policy->reclaimable_bytes_percent(reclaimable_bytes);
+ double threshold = (double) G1HeapWastePercent;
+ if (reclaimable_percent <= threshold) {
+ // We've added enough old regions that the amount of uncollected
+ // reclaimable space is at or below the waste threshold. Stop
+ // adding old regions to the CSet.
+ log_debug(gc, ergo, cset)("Finish adding old regions to CSet (reclaimable percentage not over threshold). "
+ "old %u regions, max %u regions, reclaimable: " SIZE_FORMAT "B (%1.2f%%) threshold: " UINTX_FORMAT "%%",
+ old_region_length(), max_old_cset_length, reclaimable_bytes, reclaimable_percent, G1HeapWastePercent);
+ break;
+ }
+
+ double predicted_time_ms = predict_region_elapsed_time_ms(hr);
+ if (check_time_remaining) {
+ if (predicted_time_ms > time_remaining_ms) {
+ // Too expensive for the current CSet.
+
+ if (old_region_length() >= min_old_cset_length) {
+ // We have added the minimum number of old regions to the CSet,
+ // we are done with this CSet.
+ log_debug(gc, ergo, cset)("Finish adding old regions to CSet (predicted time is too high). "
+ "predicted time: %1.2fms, remaining time: %1.2fms old %u regions, min %u regions",
+ predicted_time_ms, time_remaining_ms, old_region_length(), min_old_cset_length);
+ break;
+ }
+
+ // We'll add it anyway given that we haven't reached the
+ // minimum number of old regions.
+ expensive_region_num += 1;
+ }
+ } else {
+ if (old_region_length() >= min_old_cset_length) {
+ // In the non-auto-tuning case, we'll finish adding regions
+ // to the CSet if we reach the minimum.
+
+ log_debug(gc, ergo, cset)("Finish adding old regions to CSet (old CSet region num reached min). old %u regions, min %u regions",
+ old_region_length(), min_old_cset_length);
+ break;
+ }
+ }
+
+ // We will add this region to the CSet.
+ time_remaining_ms = MAX2(time_remaining_ms - predicted_time_ms, 0.0);
+ predicted_old_time_ms += predicted_time_ms;
+ cset_chooser()->pop(); // already have region via peek()
+ _g1h->old_set_remove(hr);
+ add_old_region(hr);
+
+ hr = cset_chooser()->peek();
+ }
+ if (hr == NULL) {
+ log_debug(gc, ergo, cset)("Finish adding old regions to CSet (candidate old regions not available)");
+ }
+
+ if (expensive_region_num > 0) {
+ // We print the information once here at the end, predicated on
+ // whether we added any apparently expensive regions or not, to
+ // avoid generating output per region.
+ log_debug(gc, ergo, cset)("Added expensive regions to CSet (old CSet region num not reached min)."
+ "old: %u regions, expensive: %u regions, min: %u regions, remaining time: %1.2fms",
+ old_region_length(), expensive_region_num, min_old_cset_length, time_remaining_ms);
+ }
+
+ cset_chooser()->verify();
+ }
+
+ stop_incremental_building();
+
+ log_debug(gc, ergo, cset)("Finish choosing CSet. old: %u regions, predicted old region time: %1.2fms, time remaining: %1.2f",
+ old_region_length(), predicted_old_time_ms, time_remaining_ms);
+
+ double non_young_end_time_sec = os::elapsedTime();
+ phase_times()->record_non_young_cset_choice_time_ms((non_young_end_time_sec - non_young_start_time_sec) * 1000.0);
+
+ QuickSort::sort(_collection_set_regions, _collection_set_cur_length, compare_region_idx, true);
+}
+上面第三行是个判断,当前是否是 mixed 回收阶段,如果不是的话其实是没有老年代什么事的,所以可以看到代码基本是从这个 if 判断if (collector_state()->in_mixed_phase()) {开始往下走的
先写到这,偏向于做笔记用,有错轻拍
Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.
-将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
-
Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
A subarray is a contiguous part of an array.
+Example 1:
--输入:l1 = [1,2,4], l2 = [1,3,4]
+
输出:[1,1,2,3,4,4]Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
-输入: l1 = [], l2 = []
+
输出: []Example 2:
++-Input: nums = [1]
Output: 1示例 3
-输入: l1 = [], l2 = [0]
+
输出: [0]Example 3:
++-Input: nums = [5,4,-1,7,8]
Output: 23简要分析
这题是 Easy 的,看着也挺简单,两个链表进行合并,就是比较下大小,可能将就点的话最好就在两个链表中原地合并
-题解代码
- -public ListNode mergeTwoLists(ListNode l1, ListNode l2) { - // 下面两个if判断了入参的边界,如果其一为null,直接返回另一个就可以了 - if (l1 == null) { - return l2; - } - if (l2 == null) { - return l1; - } - // new 一个合并后的头结点 - ListNode merged = new ListNode(); - // 这个是当前节点 - ListNode current = merged; - // 一开始给这个while加了l1和l2不全为null的条件,后面想了下不需要 - // 因为内部前两个if就是跳出条件 - while (true) { - if (l1 == null) { - // 这里其实跟开头类似,只不过这里需要将l2剩余部分接到merged链表后面 - // 所以不能是直接current = l2,这样就是把后面的直接丢了 - current.val = l2.val; - current.next = l2.next; - break; - } - if (l2 == null) { - current.val = l1.val; - current.next = l1.next; - break; - } - // 这里是两个链表都不为空的时候,就比较下大小 - if (l1.val < l2.val) { - current.val = l1.val; - l1 = l1.next; - } else { - current.val = l2.val; - l2 = l2.next; - } - // 这里是new个新的,其实也可以放在循环头上 - current.next = new ListNode(); - current = current.next; - } - current = null; - // 返回这个头结点 - return merged; - }结果
-]]>
Implement strStr().
Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.
What should we return when needle is an empty string? This is a great question to ask during an interview.
For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C’s strstr() and Java’s indexOf().
Example 1:
-Input: haystack = "hello", needle = "ll"
-Output: 2
-Example 2:
-Input: haystack = "aaaaa", needle = "bba"
-Output: -1
-Example 3:
-Input: haystack = "", needle = ""
-Output: 0
-
-字符串比较其实是写代码里永恒的主题,底层的编译器等处理肯定需要字符串对比,像 kmp 算法也是很厉害
-public int strStr(String haystack, String needle) {
- // 如果两个字符串都为空,返回 -1
- if (haystack == null || needle == null) {
- return -1;
- }
- // 如果 haystack 长度小于 needle 长度,返回 -1
- if (haystack.length() < needle.length()) {
- return -1;
- }
- // 如果 needle 为空字符串,返回 0
- if (needle.equals("")) {
- return 0;
- }
- // 如果两者相等,返回 0
- if (haystack.equals(needle)) {
- return 0;
- }
- int needleLength = needle.length();
- int haystackLength = haystack.length();
- for (int i = needleLength - 1; i <= haystackLength - 1; i++) {
- // 比较 needle 最后一个字符,倒着比较稍微节省点时间
- if (needle.charAt(needleLength - 1) == haystack.charAt(i)) {
- // 如果needle 是 1 的话直接可以返回 i 作为位置了
- if (needle.length() == 1) {
- return i;
- }
- boolean flag = true;
- // 原来比的是 needle 的最后一个位置,然后这边从倒数第二个位置开始
- int j = needle.length() - 2;
- for (; j >= 0; j--) {
- // 这里的 i- (needleLength - j) + 1 ) 比较绕,其实是外循环的 i 表示当前 i 位置的字符跟 needle 最后一个字符
- // 相同,j 在上面的循环中--,对应的 haystack 也要在 i 这个位置 -- ,对应的位置就是 i - (needleLength - j) + 1
- if (needle.charAt(j) != haystack.charAt(i - (needleLength - j) + 1)) {
- flag = false;
- break;
- }
- }
- // 循环完了之后,如果 flag 为 true 说明 从 i 开始倒着对比都相同,但是这里需要起始位置,就需要
- // i - needleLength + 1
- if (flag) {
- return i - needleLength + 1;
- }
- }
- }
- // 这里表示未找到
- return -1;
- }]]>Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
A subarray is a contiguous part of an array.
-Example 1:
---Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
-
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
Example 2:
---Input: nums = [1]
-
Output: 1
Example 3:
---Input: nums = [5,4,-1,7,8]
-
Output: 23
说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。
-public int maxSubArray(int[] nums) {
- int max = nums[0];
- int sum = nums[0];
- for (int i = 1; i < nums.length; i++) {
- // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
- sum = Math.max(nums[i], sum + nums[i]);
- // max 是用来承载最大值的
- max = Math.max(max, sum);
+说起来这个题其实非常有渊源,大学数据结构的第一个题就是这个,而最佳的算法就是传说中的 online 算法,就是遍历一次就完了,最基本的做法就是记下来所有的连续子数组,然后求出最大的那个。
+代码
public int maxSubArray(int[] nums) {
+ int max = nums[0];
+ int sum = nums[0];
+ for (int i = 1; i < nums.length; i++) {
+ // 这里最重要的就是这一行了,其实就是如果前面的 sum 是小于 0 的,那么就不需要前面的 sum,反正加上了还不如不加大
+ sum = Math.max(nums[i], sum + nums[i]);
+ // max 是用来承载最大值的
+ max = Math.max(max, sum);
}
return max;
}
]]>比如这样子
-我的对象是Entity
-public class Entity {
-
- private Long id;
-
- private Long sortValue;
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public Long getSortValue() {
- return sortValue;
- }
-
- public void setSortValue(Long sortValue) {
- this.sortValue = sortValue;
+ Leetcode 124 二叉树中的最大路径和(Binary Tree Maximum Path Sum) 题解分析
+ /2021/01/24/Leetcode-124-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C-Binary-Tree-Maximum-Path-Sum-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root.
+The path sum of a path is the sum of the node’s values in the path.
+Given the root of a binary tree, return the maximum path sum of any path.
+路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径 至少包含一个 节点,且不一定经过根节点。
+路径和 是路径中各节点值的总和。
+给你一个二叉树的根节点 root ,返回其 最大路径和
+简要分析
其实这个题目会被误解成比较简单,左子树最大的,或者右子树最大的,或者两边加一下,仔细想想都不对,其实有可能是产生于左子树中,或者右子树中,这两个都是指跟左子树根还有右子树根没关系的,这么说感觉不太容易理解,画个图
![]()
可以看到图里,其实最长路径和是左边这个子树组成的,跟根节点还有右子树完全没关系,然后再想一种情况,如果是整棵树就是图中的左子树,那么这个最长路径和就是左子树加右子树加根节点了,所以不是我一开始想得那么简单,在代码实现中也需要一些技巧
+代码
int ansNew = Integer.MIN_VALUE;
+public int maxPathSum(TreeNode root) {
+ maxSumNew(root);
+ return ansNew;
}
-}
-
-Comparator
-public class MyComparator implements Comparator {
- @Override
- public int compare(Object o1, Object o2) {
- Entity e1 = (Entity) o1;
- Entity e2 = (Entity) o2;
- if (e1.getSortValue() < e2.getSortValue()) {
- return -1;
- } else if (e1.getSortValue().equals(e2.getSortValue())) {
- return 0;
- } else {
- return 1;
- }
+
+public int maxSumNew(TreeNode root) {
+ if (root == null) {
+ return 0;
}
-}
-
-比较代码
-private static MyComparator myComparator = new MyComparator();
-
- public static void main(String[] args) {
- List<Entity> list = new ArrayList<Entity>();
- Entity e1 = new Entity();
- e1.setId(1L);
- e1.setSortValue(1L);
- list.add(e1);
- Entity e2 = new Entity();
- e2.setId(2L);
- e2.setSortValue(null);
- list.add(e2);
- Collections.sort(list, myComparator);
-
-看到这里的e2的排序值是null,在Comparator中如果要正常运行的话,就得判空之类的,这里有两点需要,一个是不想写这个MyComparator,然后也没那么好排除掉list里排序值,那么有什么办法能解决这种问题呢,应该说java的这方面真的是很强大
-![]()
-看一下nullsFirst的实现
-final static class NullComparator<T> implements Comparator<T>, Serializable {
- private static final long serialVersionUID = -7569533591570686392L;
- private final boolean nullFirst;
- // if null, non-null Ts are considered equal
- private final Comparator<T> real;
-
- @SuppressWarnings("unchecked")
- NullComparator(boolean nullFirst, Comparator<? super T> real) {
- this.nullFirst = nullFirst;
- this.real = (Comparator<T>) real;
- }
-
- @Override
- public int compare(T a, T b) {
- if (a == null) {
- return (b == null) ? 0 : (nullFirst ? -1 : 1);
- } else if (b == null) {
- return nullFirst ? 1: -1;
- } else {
- return (real == null) ? 0 : real.compare(a, b);
- }
- }
+ // 这里是个简单的递归,就是去递归左右子树,但是这里其实有个概念,当这样处理时,其实相当于把子树的内部的最大路径和已经算出来了
+ int left = maxSumNew(root.left);
+ int right = maxSumNew(root.right);
+ // 这里前面我有点没想明白,但是看到 ansNew 的比较,其实相当于,返回的是三种情况里的最大值,一个是左子树+根,一个是右子树+根,一个是单独根节点,
+ // 这样这个递归的返回才会有意义,不然像原来的方法,它可能是跳着的,但是这种情况其实是借助于 ansNew 这个全局的最大值,因为原来我觉得要比较的是
+ // left, right, left + root , right + root, root, left + right + root 这些的最大值,这里是分成了两个阶段,left 跟 right 的最大值已经在上面的
+ // 调用过程中赋值给 ansNew 了
+ int currentSum = Math.max(Math.max(root.val + left , root.val + right), root.val);
+ // 这边返回的是 currentSum,然后再用它跟 left + right + root 进行对比,然后再去更新 ans
+ // PS: 有个小点也是这边的破局点,就是这个 ansNew
+ int res = Math.max(left + right + root.val, currentSum);
+ ans = Math.max(res, ans);
+ return currentSum;
+}
-核心代码就是下面这段,其实就是帮我们把前面要做的事情做掉了,是不是挺方便的,小记一下哈
+这里非常重要的就是 ansNew 是最后的一个结果,而对于 maxSumNew 这个函数的返回值其实是需要包含了一个连续结果,因为要返回继续去算路径和,所以返回的是 currentSum,最终结果是 ansNew
+难得有个 100%,贴个图哈哈
ArrayBlockingQueue,因为这个阻塞队列是使用了锁来控制阻塞,关于并发其实有一些通用的最佳实践,就是用锁,即使是 JDK 提供的锁,也是比较耗资源的,当然这是跟不加锁的对比,同样是锁,JDK 的实现还是性能比较优秀的。常见的阻塞队列中例如 ArrayBlockingQueue 和 LinkedBlockingQueue 都有锁的身影的存在,区别在于 ArrayBlockingQueue 是一把锁,后者是两把锁,不过重点不在几把锁,这里其实是两个问题,一个是所谓的 lock free, 对于一个单生产者的 disruptor 来说,因为写入是只有一个线程的,是可以不用加锁,多生产者的时候使用的是 cas 来获取对应的写入坑位,另一个是解决“伪共享”问题,后面可以详细点分析,先介绍下使用public class LongEvent {
- private long value;
-
- public void set(long value) {
- this.value = value;
- }
-
- public long getValue() {
- return value;
- }
-
- public void setValue(long value) {
- this.value = value;
- }
-}
-事件生产
-public class LongEventFactory implements EventFactory<LongEvent>
-{
- public LongEvent newInstance()
- {
- return new LongEvent();
- }
-}
-事件处理器
-public class LongEventHandler implements EventHandler<LongEvent> {
-
- // event 事件,
- // sequence 当前的序列
- // 是否当前批次最后一个数据
- public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
- {
- String str = String.format("long event : %s l:%s b:%s", event.getValue(), sequence, endOfBatch);
- System.out.println(str);
- }
-}
-
-主方法代码
-package disruptor;
-
-import com.lmax.disruptor.RingBuffer;
-import com.lmax.disruptor.dsl.Disruptor;
-import com.lmax.disruptor.util.DaemonThreadFactory;
-
-import java.nio.ByteBuffer;
-
-public class LongEventMain
-{
- public static void main(String[] args) throws Exception
- {
- // 这个需要是 2 的幂次,这样在定位的时候只需要位移操作,也能减少各种计算操作
- int bufferSize = 1024;
-
- Disruptor<LongEvent> disruptor =
- new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
-
- // 类似于注册处理器
- disruptor.handleEventsWith(new LongEventHandler());
- // 或者直接用 lambda
- disruptor.handleEventsWith((event, sequence, endOfBatch) ->
- System.out.println("Event: " + event));
- // 启动我们的 disruptor
- disruptor.start();
-
-
- RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
- ByteBuffer bb = ByteBuffer.allocate(8);
- for (long l = 0; true; l++)
- {
- bb.putLong(0, l);
- // 生产事件
- ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
- Thread.sleep(1000);
- }
- }
-}
-运行下可以看到运行结果
这里其实就只是最简单的使用,生产者只有一个,然后也不是批量的。
A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root.
-The path sum of a path is the sum of the node’s values in the path.
-Given the root of a binary tree, return the maximum path sum of any path.
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径 至少包含一个 节点,且不一定经过根节点。
-路径和 是路径中各节点值的总和。
-给你一个二叉树的根节点 root ,返回其 最大路径和
其实这个题目会被误解成比较简单,左子树最大的,或者右子树最大的,或者两边加一下,仔细想想都不对,其实有可能是产生于左子树中,或者右子树中,这两个都是指跟左子树根还有右子树根没关系的,这么说感觉不太容易理解,画个图
可以看到图里,其实最长路径和是左边这个子树组成的,跟根节点还有右子树完全没关系,然后再想一种情况,如果是整棵树就是图中的左子树,那么这个最长路径和就是左子树加右子树加根节点了,所以不是我一开始想得那么简单,在代码实现中也需要一些技巧
int ansNew = Integer.MIN_VALUE;
-public int maxPathSum(TreeNode root) {
- maxSumNew(root);
- return ansNew;
- }
-
-public int maxSumNew(TreeNode root) {
- if (root == null) {
- return 0;
- }
- // 这里是个简单的递归,就是去递归左右子树,但是这里其实有个概念,当这样处理时,其实相当于把子树的内部的最大路径和已经算出来了
- int left = maxSumNew(root.left);
- int right = maxSumNew(root.right);
- // 这里前面我有点没想明白,但是看到 ansNew 的比较,其实相当于,返回的是三种情况里的最大值,一个是左子树+根,一个是右子树+根,一个是单独根节点,
- // 这样这个递归的返回才会有意义,不然像原来的方法,它可能是跳着的,但是这种情况其实是借助于 ansNew 这个全局的最大值,因为原来我觉得要比较的是
- // left, right, left + root , right + root, root, left + right + root 这些的最大值,这里是分成了两个阶段,left 跟 right 的最大值已经在上面的
- // 调用过程中赋值给 ansNew 了
- int currentSum = Math.max(Math.max(root.val + left , root.val + right), root.val);
- // 这边返回的是 currentSum,然后再用它跟 left + right + root 进行对比,然后再去更新 ans
- // PS: 有个小点也是这边的破局点,就是这个 ansNew
- int res = Math.max(left + right + root.val, currentSum);
- ans = Math.max(res, ans);
- return currentSum;
-}
-
-这里非常重要的就是 ansNew 是最后的一个结果,而对于 maxSumNew 这个函数的返回值其实是需要包含了一个连续结果,因为要返回继续去算路径和,所以返回的是 currentSum,最终结果是 ansNew
-难得有个 100%,贴个图哈哈
Given an integer array nums, return the sum of floor(nums[i] / nums[j]) for all pairs of indices 0 <= i, j < nums.length in the array. Since the answer may be too large, return it modulo 10^9 + 7.
The floor() function returns the integer part of the division.
对应中文
给你一个整数数组 nums ,请你返回所有下标对 0 <= i, j < nums.length 的 floor(nums[i] / nums[j]) 结果之和。由于答案可能会很大,请你返回答案对10^9 + 7 取余 的结果。
函数 floor() 返回输入数字的整数部分。
++Input: nums = [2,5,9]
+
Output: 10
Explanation:
floor(2 / 5) = floor(2 / 9) = floor(5 / 9) = 0
floor(2 / 2) = floor(5 / 5) = floor(9 / 9) = 1
floor(5 / 2) = 2
floor(9 / 2) = 4
floor(9 / 5) = 1
We calculate the floor of the division for every pair of indices in the array then sum them up.
++Input: nums = [7,7,7,7,7,7,7]
+
Output: 49
1 <= nums.length <= 10^51 <= nums[i] <= 10^5这题不愧是 hard,要不是看了讨论区的一个大神的解答感觉从头做得想好久,
主要是两点,对于任何一个在里面的数,随便举个例子是 k,最简单的就是循环所有数对 k 除一下,
这样效率会很低,那么对于 k 有什么规律呢,就是对于所有小于 k 的数,往下取整都是 0,所以不用考虑,
对于所有大于 k 的数我们可以分成一个个的区间,[k,2k-1),[2k,3k-1),[3k,4k-1)……对于这些区间的
除了 k 往下取整,每个区间内的都是一样的,所以可以简化为对于任意一个 k,我只要知道与k 相同的有多少个,然后比 k 大的各个区间各有多少个数就可以了
static final int MAXE5 = 100_000;
+
+static final int MODULUSE9 = 1_000_000_000 + 7;
+
+public int sumOfFlooredPairs(int[] nums) {
+ int[] counts = new int[MAXE5+1];
+ for (int num : nums) {
+ counts[num]++;
+ }
+ // 这里就是很巧妙的给后一个加上前一个的值,这样其实前后任意两者之差就是这中间的元素数量
+ for (int i = 1; i <= MAXE5; i++) {
+ counts[i] += counts[i - 1];
+ }
+ long total = 0;
+ for (int i = 1; i <= MAXE5; i++) {
+ long sum = 0;
+ if (counts[i] == counts[i-1]) {
+ continue;
+ }
+ for (int j = 1; i*j <= MAXE5; j++) {
+ int min = i * j - 1;
+ int upper = i * (j + 1) - 1;
+ // 在每一个区间内的数量,
+ sum += (counts[Math.min(upper, MAXE5)] - counts[min]) * (long)j;
+ }
+ // 左边乘数的数量,即 i 位置的元素数量
+ total = (total + (sum % MODULUSE9 ) * (counts[i] - counts[i-1])) % MODULUSE9;
+ }
+ return (int)total;
+}
+
+贴出来大神的解析,解析
+
Given a singly linked list, determine if it is a palindrome.
给定一个单向链表,判断是否是回文链表
Input: 1->2
Output: false
Input: 1->2->2->1
Output: true
Follow up:
Could you do it in O(n) time and O(1) space?
首先这是个单向链表,如果是双向的就可以一个从头到尾,一个从尾到头,显然那样就没啥意思了,然后想过要不找到中点,然后用一个栈,把前一半塞进栈里,但是这种其实也比较麻烦,比如长度是奇偶数,然后如何找到中点,这倒是可以借助于双指针,还是比较麻烦,再想一想,回文链表,就跟最开始的一样,链表只有单向的,我用个栈不就可以逆向了么,先把链表整个塞进栈里,然后在一个个 pop 出来跟链表从头开始比较,全对上了就是回文了
-/**
- * Definition for singly-linked list.
- * public class ListNode {
- * int val;
- * ListNode next;
- * ListNode() {}
- * ListNode(int val) { this.val = val; }
- * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
- * }
- */
-class Solution {
- public boolean isPalindrome(ListNode head) {
- if (head == null) {
- return true;
+ Leetcode 20 有效的括号 ( Valid Parentheses *Easy* ) 题解分析
+ /2022/07/02/Leetcode-20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7-Valid-Parentheses-Easy-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
+An input string is valid if:
+
+- Open brackets must be closed by the same type of brackets.
+- Open brackets must be closed in the correct order.
+
+示例
Example 1:
+Input: s = “()”
Output: true
+
+Example 2:
+Input: s = “()[]{}”
Output: true
+
+Example 3:
+Input: s = “(]”
Output: false
+
+Constraints:
+1 <= s.length <= 10^4
+s consists of parentheses only '()[]{}'.
+
+解析
easy题,并且看起来也是比较简单的,三种括号按对匹配,直接用栈来做,栈里面存的是括号的类型,如果是左括号,就放入栈中,如果是右括号,就把栈顶的元素弹出,如果弹出的元素不是左括号,就返回false,如果弹出的元素是左括号,就继续往下走,如果遍历完了,如果栈里面还有元素,就返回false,如果遍历完了,如果栈里面没有元素,就返回true
+代码
class Solution {
+ public boolean isValid(String s) {
+
+ if (s.length() % 2 != 0) {
+ return false;
}
- ListNode tail = head;
- LinkedList<Integer> stack = new LinkedList<>();
- // 这里就是一个循环,将所有元素依次压入栈
- while (tail != null) {
+ Stack<String> stk = new Stack<>();
+ for (int i = 0; i < s.length(); i++) {
+ if (s.charAt(i) == '{' || s.charAt(i) == '(' || s.charAt(i) == '[') {
+ stk.push(String.valueOf(s.charAt(i)));
+ continue;
+ }
+ if (s.charAt(i) == '}') {
+ if (stk.isEmpty()) {
+ return false;
+ }
+ String cur = stk.peek();
+ if (cur.charAt(0) != '{') {
+ return false;
+ } else {
+ stk.pop();
+ }
+ continue;
+ }
+ if (s.charAt(i) == ']') {
+ if (stk.isEmpty()) {
+ return false;
+ }
+ String cur = stk.peek();
+ if (cur.charAt(0) != '[') {
+ return false;
+ } else {
+ stk.pop();
+ }
+ continue;
+ }
+ if (s.charAt(i) == ')') {
+ if (stk.isEmpty()) {
+ return false;
+ }
+ String cur = stk.peek();
+ if (cur.charAt(0) != '(') {
+ return false;
+ } else {
+ stk.pop();
+ }
+ continue;
+ }
+
+ }
+ return stk.size() == 0;
+ }
+}
+
+]]>
+
+ Java
+ leetcode
+
+
+ leetcode
+ java
+
+ Given a singly linked list, determine if it is a palindrome.
给定一个单向链表,判断是否是回文链表
Input: 1->2
Output: false
Input: 1->2->2->1
Output: true
Follow up:
Could you do it in O(n) time and O(1) space?
首先这是个单向链表,如果是双向的就可以一个从头到尾,一个从尾到头,显然那样就没啥意思了,然后想过要不找到中点,然后用一个栈,把前一半塞进栈里,但是这种其实也比较麻烦,比如长度是奇偶数,然后如何找到中点,这倒是可以借助于双指针,还是比较麻烦,再想一想,回文链表,就跟最开始的一样,链表只有单向的,我用个栈不就可以逆向了么,先把链表整个塞进栈里,然后在一个个 pop 出来跟链表从头开始比较,全对上了就是回文了
+/**
+ * Definition for singly-linked list.
+ * public class ListNode {
+ * int val;
+ * ListNode next;
+ * ListNode() {}
+ * ListNode(int val) { this.val = val; }
+ * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
+ * }
+ */
+class Solution {
+ public boolean isPalindrome(ListNode head) {
+ if (head == null) {
+ return true;
+ }
+ ListNode tail = head;
+ LinkedList<Integer> stack = new LinkedList<>();
+ // 这里就是一个循环,将所有元素依次压入栈
+ while (tail != null) {
stack.push(tail.val);
tail = tail.next;
}
@@ -4000,68 +4573,6 @@ Output: [8,9,9,9,0,0,0,1]Given an integer array nums, return the sum of floor(nums[i] / nums[j]) for all pairs of indices 0 <= i, j < nums.length in the array. Since the answer may be too large, return it modulo 10^9 + 7.
The floor() function returns the integer part of the division.
对应中文
给你一个整数数组 nums ,请你返回所有下标对 0 <= i, j < nums.length 的 floor(nums[i] / nums[j]) 结果之和。由于答案可能会很大,请你返回答案对10^9 + 7 取余 的结果。
函数 floor() 返回输入数字的整数部分。
--Input: nums = [2,5,9]
-
Output: 10
Explanation:
floor(2 / 5) = floor(2 / 9) = floor(5 / 9) = 0
floor(2 / 2) = floor(5 / 5) = floor(9 / 9) = 1
floor(5 / 2) = 2
floor(9 / 2) = 4
floor(9 / 5) = 1
We calculate the floor of the division for every pair of indices in the array then sum them up.
--Input: nums = [7,7,7,7,7,7,7]
-
Output: 49
1 <= nums.length <= 10^51 <= nums[i] <= 10^5这题不愧是 hard,要不是看了讨论区的一个大神的解答感觉从头做得想好久,
主要是两点,对于任何一个在里面的数,随便举个例子是 k,最简单的就是循环所有数对 k 除一下,
这样效率会很低,那么对于 k 有什么规律呢,就是对于所有小于 k 的数,往下取整都是 0,所以不用考虑,
对于所有大于 k 的数我们可以分成一个个的区间,[k,2k-1),[2k,3k-1),[3k,4k-1)……对于这些区间的
除了 k 往下取整,每个区间内的都是一样的,所以可以简化为对于任意一个 k,我只要知道与k 相同的有多少个,然后比 k 大的各个区间各有多少个数就可以了
static final int MAXE5 = 100_000;
-
-static final int MODULUSE9 = 1_000_000_000 + 7;
-
-public int sumOfFlooredPairs(int[] nums) {
- int[] counts = new int[MAXE5+1];
- for (int num : nums) {
- counts[num]++;
- }
- // 这里就是很巧妙的给后一个加上前一个的值,这样其实前后任意两者之差就是这中间的元素数量
- for (int i = 1; i <= MAXE5; i++) {
- counts[i] += counts[i - 1];
- }
- long total = 0;
- for (int i = 1; i <= MAXE5; i++) {
- long sum = 0;
- if (counts[i] == counts[i-1]) {
- continue;
- }
- for (int j = 1; i*j <= MAXE5; j++) {
- int min = i * j - 1;
- int upper = i * (j + 1) - 1;
- // 在每一个区间内的数量,
- sum += (counts[Math.min(upper, MAXE5)] - counts[min]) * (long)j;
- }
- // 左边乘数的数量,即 i 位置的元素数量
- total = (total + (sum % MODULUSE9 ) * (counts[i] - counts[i-1])) % MODULUSE9;
- }
- return (int)total;
-}
-
-贴出来大神的解析,解析
-
Given a string s, find the length of the longest substring without repeating characters.
Input: s = "abcabcbb"
+Output: 3
+Explanation: The answer is "abc", with the length of 3.
+
+Input: s = "bbbbb"
+Output: 1
+Explanation: The answer is "b", with the length of 1.
+Input: s = "pwwkew"
+Output: 3
+Explanation: The answer is "wke", with the length of 3.
+Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
+Input: s = ""
+Output: 0
+
+就是一个最长不重复的字符串长度,因为也是中等难度的题,不太需要特别复杂的思考,最基本的就是O(N*N)两重循环,不过显然不太好,万一超时间,还有一种就是线性复杂度的了,这个就是需要搞定一个思路,比如字符串时 abcdefgaqwrty,比如遍历到第二个a的时候其实不用再从头去遍历了,只要把前面那个a给排除掉,继续往下算就好了
class Solution {
+ Map<String, Integer> counter = new HashMap<>();
+ public int lengthOfLongestSubstring(String s) {
+ int length = s.length();
+ // 当前的长度
+ int subStringLength = 0;
+ // 最长的长度
+ int maxSubStringLength = 0;
+ // 考虑到重复的位置已经被跳过的情况,即已经在当前长度的字符串范围之前的重复字符不需要回溯
+ int lastDuplicatePos = -1;
+ for (int i = 0; i < length; i++) {
+ // 使用 map 存储字符和上一次出现的位置,如果存在并且大于上一次重复位置
+ if (counter.get(String.valueOf(s.charAt(i))) != null && counter.get(String.valueOf(s.charAt(i))) > lastDuplicatePos) {
+ // 记录重复位置
+ lastDuplicatePos = counter.get(String.valueOf(s.charAt(i)));
+ // 重置不重复子串的长度,减去重复起点
+ subStringLength = i - counter.get(String.valueOf(s.charAt(i))) - 1;
+ // 替换当前位置
+ counter.replace(String.valueOf(s.charAt(i)), i);
+ } else {
+ // 如果不存在就直接 put
+ counter.put(String.valueOf(s.charAt(i)), i);
+ }
+ // 长度累加
+ subStringLength++;
+ if (subStringLength > maxSubStringLength) {
+ // 简单替换
+ maxSubStringLength = subStringLength;
+ }
+ }
+ return maxSubStringLength;
+ }
+}
+注释应该写的比较清楚了。
+]]>Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
An input string is valid if:
---Input: s = “()”
-
Output: true
-Input: s = “()[]{}”
+
Output: trueLeetcode 4 寻找两个正序数组的中位数 ( Median of Two Sorted Arrays *Hard* ) 题解分析 +/2022/03/27/Leetcode-4-%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0-Median-of-Two-Sorted-Arrays-Hard-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/ +题目介绍 -给定两个大小分别为
+m和n的正序(从小到大)数组nums1和nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为
+O(log (m+n))。示例 1:
+-输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2Example 3:
-Input: s = “(]”
+
Output: false示例 2:
+-输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5Constraints:
-
-- -
1 <= s.length <= 10^4- -
sconsists of parentheses only'()[]{}'.解析
easy题,并且看起来也是比较简单的,三种括号按对匹配,直接用栈来做,栈里面存的是括号的类型,如果是左括号,就放入栈中,如果是右括号,就把栈顶的元素弹出,如果弹出的元素不是左括号,就返回false,如果弹出的元素是左括号,就继续往下走,如果遍历完了,如果栈里面还有元素,就返回false,如果遍历完了,如果栈里面没有元素,就返回true
-代码
class Solution { - public boolean isValid(String s) { - - if (s.length() % 2 != 0) { - return false; +分析与题解
这个题也是我随机出来的,之前都是随机到 easy 的,而且是序号这么靠前的,然后翻一下,之前应该是用 C++做过的,具体的方法其实可以从他的算法时间复杂度要求看出来,大概率是要二分法这种,后面就结合代码来讲了
+- -]]>public double findMedianSortedArrays(int[] nums1, int[] nums2) { + int n1 = nums1.length; + int n2 = nums2.length; + if (n1 > n2) { + return findMedianSortedArrays(nums2, nums1); } - Stack<String> stk = new Stack<>(); - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == '{' || s.charAt(i) == '(' || s.charAt(i) == '[') { - stk.push(String.valueOf(s.charAt(i))); - continue; - } - if (s.charAt(i) == '}') { - if (stk.isEmpty()) { - return false; - } - String cur = stk.peek(); - if (cur.charAt(0) != '{') { - return false; - } else { - stk.pop(); - } - continue; - } - if (s.charAt(i) == ']') { - if (stk.isEmpty()) { - return false; - } - String cur = stk.peek(); - if (cur.charAt(0) != '[') { - return false; - } else { - stk.pop(); - } - continue; - } - if (s.charAt(i) == ')') { - if (stk.isEmpty()) { - return false; - } - String cur = stk.peek(); - if (cur.charAt(0) != '(') { - return false; - } else { - stk.pop(); - } - continue; - } - - } - return stk.size() == 0; - } -}- -Java -leetcode -- -leetcode -java -
Given a string s, find the length of the longest substring without repeating characters.
Input: s = "abcabcbb"
-Output: 3
-Explanation: The answer is "abc", with the length of 3.
-
-Input: s = "bbbbb"
-Output: 1
-Explanation: The answer is "b", with the length of 1.
-Input: s = "pwwkew"
-Output: 3
-Explanation: The answer is "wke", with the length of 3.
-Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
-Input: s = ""
-Output: 0
-就是一个最长不重复的字符串长度,因为也是中等难度的题,不太需要特别复杂的思考,最基本的就是O(N*N)两重循环,不过显然不太好,万一超时间,还有一种就是线性复杂度的了,这个就是需要搞定一个思路,比如字符串时 abcdefgaqwrty,比如遍历到第二个a的时候其实不用再从头去遍历了,只要把前面那个a给排除掉,继续往下算就好了
class Solution {
- Map<String, Integer> counter = new HashMap<>();
- public int lengthOfLongestSubstring(String s) {
- int length = s.length();
- // 当前的长度
- int subStringLength = 0;
- // 最长的长度
- int maxSubStringLength = 0;
- // 考虑到重复的位置已经被跳过的情况,即已经在当前长度的字符串范围之前的重复字符不需要回溯
- int lastDuplicatePos = -1;
- for (int i = 0; i < length; i++) {
- // 使用 map 存储字符和上一次出现的位置,如果存在并且大于上一次重复位置
- if (counter.get(String.valueOf(s.charAt(i))) != null && counter.get(String.valueOf(s.charAt(i))) > lastDuplicatePos) {
- // 记录重复位置
- lastDuplicatePos = counter.get(String.valueOf(s.charAt(i)));
- // 重置不重复子串的长度,减去重复起点
- subStringLength = i - counter.get(String.valueOf(s.charAt(i))) - 1;
- // 替换当前位置
- counter.replace(String.valueOf(s.charAt(i)), i);
- } else {
- // 如果不存在就直接 put
- counter.put(String.valueOf(s.charAt(i)), i);
- }
- // 长度累加
- subStringLength++;
- if (subStringLength > maxSubStringLength) {
- // 简单替换
- maxSubStringLength = subStringLength;
- }
- }
- return maxSubStringLength;
- }
-}
-注释应该写的比较清楚了。
-]]>You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.
如图,这道题以前做过,其实一看有点蒙,好像规则很容易描述,但是代码很难写,因为要类似于贪吃蛇那样,后来想着应该会有一些特殊的技巧,比如翻转等
直接上码
-public void rotate(int[][] matrix) {
- // 这里真的傻了,长宽应该是一致的,所以取一次就够了
- int lengthX = matrix[0].length;
- int lengthY = matrix.length;
- int temp;
- System.out.println(lengthY - (lengthY % 2) / 2);
- // 这里除错了,应该是减掉余数再除 2
-// for (int i = 0; i < lengthY - (lengthY % 2) / 2; i++) {
- /**
- * 1 2 3 7 8 9
- * 4 5 6 => 4 5 6 先沿着 4 5 6 上下交换
- * 7 8 9 1 2 3
- */
- for (int i = 0; i < (lengthY - (lengthY % 2)) / 2; i++) {
- for (int j = 0; j < lengthX; j++) {
- temp = matrix[i][j];
- matrix[i][j] = matrix[lengthY-i-1][j];
- matrix[lengthY-i-1][j] = temp;
+ // 找到两个数组的中点下标
+ int k = (n1 + n2 + 1 ) / 2;
+ // 使用一个类似于二分法的查找方法
+ // 起始值就是 num1 的头跟尾
+ int left = 0;
+ int right = n1;
+ while (left < right) {
+ // m1 表示我取的是 nums1 的中点,即二分法的方式
+ int m1 = left + (right - left) / 2;
+ // *** 这里是重点,因为这个问题也可以转换成找成 n1 + n2 那么多个数中的前 (n1 + n2 + 1) / 2 个
+ // *** 因为两个数组都是排好序的,那么我从 num1 中取了 m1 个,从 num2 中就是去 k - m1 个
+ // *** 但是不知道取出来大小是否正好是整体排序的第 (n1 + n2 + 1) / 2 个,所以需要二分法上下对比
+ int m2 = k - m1;
+ // 如果 nums1[m1] 小,那我在第一个数组 nums1 的二分查找就要把左端点改成前一次的中点 + 1 (不然就进死循环了
+ if (nums1[m1] < nums2[m2 - 1]) {
+ left = m1 + 1;
+ } else {
+ right = m1;
}
}
- /**
- * 7 8 9 7 4 1
- * 4 5 6 => 8 5 2 这里再沿着 7 5 3 这条对角线交换
- * 1 2 3 9 6 3
- */
- for (int i = 0; i < lengthX; i++) {
- for (int j = 0; j <= i; j++) {
- if (i == j) {
- continue;
- }
- temp = matrix[i][j];
- matrix[i][j] = matrix[j][i];
- matrix[j][i] = temp;
- }
+ // 因为对比后其实我们只是拿到了一个位置,具体哪个是第 k 个就需要继续判断
+ int m1 = left;
+ int m2 = k - left;
+ // 如果 m1 或者 m2 有小于等于 0 的,那这个值可以先抛弃
+ // m1 如果等于 0,就是 num1[0] 都比 nums2 中所有值都要大
+ // m2 等于 0 的话 刚好相反
+ // 可以这么推断,当其中一个是 0 的时候那么另一个 mx 值肯定是> 0 的,那么就是取的对应的这个下标的值
+ int c1 = Math.max( m1 <= 0 ? Integer.MIN_VALUE : nums1[m1 - 1] , m2 <= 0 ? Integer.MIN_VALUE : nums2[m2 - 1]);
+ // 如果两个数组的元素数量和是奇数,那就直接可以返回了,因为 m1 + m2 就是 k, 如果是一个数组,那这个元素其实就是 nums[k - 1]
+ // 如果 m1 或者 m2 是 0,那另一个就是 k,取 mx - 1的下标就等于是 k - 1
+ // 如果都不是 0,那就是取的了 nums1[m1 - 1] 与 nums2[m2 - 1]中的较大者,如果取得是后者,那么也就是 m1 + m2 - 1 的下标就是 k - 1
+ if ((n1 + n2) % 2 == 1) {
+ return c1;
}
- }
-还没到可以直接归纳题目类型的水平,主要是几年前做过,可能有那么点模糊的记忆,当然应该也有直接转的方法
+ // 如果是偶数个,那还要取两个数组后面的较小者,然后求平均值 + int c2 = Math.min(m1 >= n1 ? Integer.MAX_VALUE : nums1[m1], m2 >= n2 ? Integer.MAX_VALUE : nums2[m2]); + return (c1 + c2) / 2.0; + } +前面考虑的方法还是比较繁琐,考虑了两个数组的各种交叉情况,后面这个参考了一些网上的解法,代码比较简洁,但是可能不容易一下子就搞明白,所以配合了比较多的注释。
]]>Given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.
Example 1:
---Input: nums = [4,3,2,3,5,2,1], k = 4
-
Output: true
Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.
Example 2:
---Input: nums = [1,2,3,4], k = 3
-
Output: false
Constraints:
-看到这个题一开始以为挺简单,但是仔细想想问题还是挺多的,首先是分成 k 组,但是数量不限,应该需要用到回溯的方式,同时对于时间和空间复杂度也有要求,一开始这个代码是超时的,我也试了下 leetcode 上 discussion 里 vote 最高的提交也是超时的,不过看 discussion 里的帖子,貌似是后面加了一些条件,可以帮忙提高执行效率,第三条提示不太清楚意图,具体可以看下代码
-public boolean canPartitionKSubsets(int[] nums, int k) {
- if (k == 1) {
- return true;
- }
- int sum = 0, n;
- n = nums.length;
- for (int num : nums) {
- sum += num;
- }
- if (sum % k != 0) {
- return false;
- }
-
- int avg = sum / k;
- // 排序
- Arrays.sort(nums);
- // 做个前置判断,如果最大值超过分组平均值了就可以返回 false 了
- if (nums[n - 1] > avg) {
- return false;
- }
- // 这里取了个巧,先将数组中元素就等于分组平均值的直接排除了
- int calculated = 0;
- for (int i = n - 1; i > 0; i--) {
- if (nums[i] == avg) {
- k--;
- calculated++;
- }
- }
-
- int[] bucket = new int[k];
- // 初始化 bucket
- for (int i = 0; i < k; i++) {
- bucket[i] = avg;
+ Leetcode 48 旋转图像(Rotate Image) 题解分析
+ /2021/05/01/Leetcode-48-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F-Rotate-Image-%E9%A2%98%E8%A7%A3%E5%88%86%E6%9E%90/
+ 题目介绍You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
+You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.
![]()
如图,这道题以前做过,其实一看有点蒙,好像规则很容易描述,但是代码很难写,因为要类似于贪吃蛇那样,后来想着应该会有一些特殊的技巧,比如翻转等
+代码
直接上码
+public void rotate(int[][] matrix) {
+ // 这里真的傻了,长宽应该是一致的,所以取一次就够了
+ int lengthX = matrix[0].length;
+ int lengthY = matrix.length;
+ int temp;
+ System.out.println(lengthY - (lengthY % 2) / 2);
+ // 这里除错了,应该是减掉余数再除 2
+// for (int i = 0; i < lengthY - (lengthY % 2) / 2; i++) {
+ /**
+ * 1 2 3 7 8 9
+ * 4 5 6 => 4 5 6 先沿着 4 5 6 上下交换
+ * 7 8 9 1 2 3
+ */
+ for (int i = 0; i < (lengthY - (lengthY % 2)) / 2; i++) {
+ for (int j = 0; j < lengthX; j++) {
+ temp = matrix[i][j];
+ matrix[i][j] = matrix[lengthY-i-1][j];
+ matrix[lengthY-i-1][j] = temp;
+ }
+ }
+
+ /**
+ * 7 8 9 7 4 1
+ * 4 5 6 => 8 5 2 这里再沿着 7 5 3 这条对角线交换
+ * 1 2 3 9 6 3
+ */
+ for (int i = 0; i < lengthX; i++) {
+ for (int j = 0; j <= i; j++) {
+ if (i == j) {
+ continue;
+ }
+ temp = matrix[i][j];
+ matrix[i][j] = matrix[j][i];
+ matrix[j][i] = temp;
+ }
+ }
+ }
+还没到可以直接归纳题目类型的水平,主要是几年前做过,可能有那么点模糊的记忆,当然应该也有直接转的方法
+]]>
+
+ Java
+ leetcode
+ Rotate Image
+
+
+ leetcode
+ java
+ 题解
+ Rotate Image
+ 矩阵
+
+ Given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.
Example 1:
+++Input: nums = [4,3,2,3,5,2,1], k = 4
+
Output: true
Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.
Example 2:
+++Input: nums = [1,2,3,4], k = 3
+
Output: false
Constraints:
+看到这个题一开始以为挺简单,但是仔细想想问题还是挺多的,首先是分成 k 组,但是数量不限,应该需要用到回溯的方式,同时对于时间和空间复杂度也有要求,一开始这个代码是超时的,我也试了下 leetcode 上 discussion 里 vote 最高的提交也是超时的,不过看 discussion 里的帖子,貌似是后面加了一些条件,可以帮忙提高执行效率,第三条提示不太清楚意图,具体可以看下代码
+public boolean canPartitionKSubsets(int[] nums, int k) {
+ if (k == 1) {
+ return true;
+ }
+ int sum = 0, n;
+ n = nums.length;
+ for (int num : nums) {
+ sum += num;
+ }
+ if (sum % k != 0) {
+ return false;
+ }
+
+ int avg = sum / k;
+ // 排序
+ Arrays.sort(nums);
+ // 做个前置判断,如果最大值超过分组平均值了就可以返回 false 了
+ if (nums[n - 1] > avg) {
+ return false;
+ }
+ // 这里取了个巧,先将数组中元素就等于分组平均值的直接排除了
+ int calculated = 0;
+ for (int i = n - 1; i > 0; i--) {
+ if (nums[i] == avg) {
+ k--;
+ calculated++;
+ }
+ }
+
+ int[] bucket = new int[k];
+ // 初始化 bucket
+ for (int i = 0; i < k; i++) {
+ bucket[i] = avg;
}
// 提前做下边界判断
@@ -4609,6 +5104,58 @@ maxR[n -java
You are given an integer array nums where the largest integer is unique.
Determine whether the largest element in the array is at least twice as much as every other number in the array. If it is, return the index of the largest element, or return -1 otherwise.
确认在数组中的最大数是否是其余任意数的两倍大及以上,如果是返回索引,如果不是返回-1
++Input: nums = [3,6,1,0]
+
Output: 1
Explanation: 6 is the largest integer.
For every other number in the array x, 6 is at least twice as big as x.
The index of value 6 is 1, so we return 1.
++Input: nums = [1,2,3,4]
+
Output: -1
Explanation: 4 is less than twice the value of 3, so we return -1.
2 <= nums.length <= 500 <= nums[i] <= 100nums is unique.这个题easy是题意也比较简单,找最大值,并且最大值是其他任意值的两倍及以上,其实就是找最大值跟次大值,比较下就好了
+public int dominantIndex(int[] nums) {
+ int largest = Integer.MIN_VALUE;
+ int second = Integer.MIN_VALUE;
+ int largestIndex = -1;
+ for (int i = 0; i < nums.length; i++) {
+ // 如果有最大的就更新,同时更新最大值和第二大的
+ if (nums[i] > largest) {
+ second = largest;
+ largest = nums[i];
+ largestIndex = i;
+ } else if (nums[i] > second) {
+ // 没有超过最大的,但是比第二大的更大就更新第二大的
+ second = nums[i];
+ }
+ }
+
+ // 判断下是否符合题目要求,要是所有值的两倍及以上
+ if (largest >= 2 * second) {
+ return largestIndex;
+ } else {
+ return -1;
+ }
+}
+第一次错了是把第二大的情况只考虑第一种,也有可能最大值完全没经过替换就变成最大值了
void CTestDialog::OnBnClickedOk()
-{
- CString m_SrcTest;
- int nIndex = m_CbTest.GetCurSel();
- m_CbTest.GetLBText(nIndex, m_SrcTest);
- OnOK();
-}
-
-模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。
00000000000000000000000000001011, so the function should return 3.从1位到2位到4位逐步的交换
-int hammingWeight(uint32_t n) {
- const uint32_t m1 = 0x55555555; //binary: 0101...
- const uint32_t m2 = 0x33333333; //binary: 00110011..
- const uint32_t m4 = 0x0f0f0f0f; //binary: 4 zeros, 4 ones ...
- const uint32_t m8 = 0x00ff00ff; //binary: 8 zeros, 8 ones ...
- const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...
-
- n = (n & m1 ) + ((n >> 1) & m1 ); //put count of each 2 bits into those 2 bits
- n = (n & m2 ) + ((n >> 2) & m2 ); //put count of each 4 bits into those 4 bits
- n = (n & m4 ) + ((n >> 4) & m4 ); //put count of each 8 bits into those 8 bits
- n = (n & m8 ) + ((n >> 8) & m8 ); //put count of each 16 bits into those 16 bits
- n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits
- return n;
+ MFC 模态对话框
+ /2014/12/24/MFC%20%E6%A8%A1%E6%80%81%E5%AF%B9%E8%AF%9D%E6%A1%86/
+ void CTestDialog::OnBnClickedOk()
+{
+ CString m_SrcTest;
+ int nIndex = m_CbTest.GetCurSel();
+ m_CbTest.GetLBText(nIndex, m_SrcTest);
+ OnOK();
+}
-}]]>模态对话框弹出确定后,在弹出对话框时新建的类及其变量会存在,但是对于其中的控件
对象无法调用函数,即如果要在主对话框中获得弹出对话框的Combo box选中值的话,需
要在弹出 对话框的确定函数内将其值取出,赋值给弹出对话框的公有变量,这样就可以
在主对话框类得到值。
redis 分布式锁 redlock 的实现,简单记录下,
-原先我对 redis 锁的概念就是加锁使用 setnx,解锁使用 lua 脚本,但是 setnx 具体是啥,lua 脚本是啥不是很清楚
首先简单思考下这个问题,首先为啥不是先 get 一下 key 存不存在,然后再 set 一个 key value,因为加锁这个操作我们是要保证两点,一个是不能中途被打断,也就是说要原子性,如果是先 get 一下 key,如果不存在再 set 值的话,那就不是原子操作了;第二个是可不可以直接 set 值呢,显然不行,锁要保证唯一性,有且只能有一个线程或者其他应用单位获得该锁,正好 setnx 给了我们这种原子命令
然后是 setnx 的键和值分别是啥,键比较容易想到是要锁住的资源,比如 user_id, 这里有个我自己之前比较容易陷进去的误区,但是这个误区后
面再说,这里其实是把user_id 作为要锁住的资源,在我获得锁的时候别的线程不允许操作,以此保证业务的正确性,不会被多个线程同时修改,确定了键,再来看看值是啥,其实原先我认为值是啥都没关系,我只要锁住了,光键就够我用了,但是考虑下多个线程的问题,如果我这个线程加了锁,然后我因为 gc 停顿等原因卡死了,这个时候redis 的锁或者说就是 redis 的缓存已经过期了,这时候另一个线程获得锁成功,然后我这个线程又活过来了,然后我就仍然认为我拿着锁,我去对数据进行修改或者释放锁,是不是就出现问题了,所以是不是我们还需要一个东西来区分这个锁是哪个线程加的,所以我们可以将值设置成为一个线程独有识别的值,至少在相对长的一段时间内不会重复。
上面其实还有两个问题,一个是当 gc 超时时,我这个线程如何知道我手里的锁已经过期了,一种方法是我在加好锁之后就维护一个超时时间,这里其实还有个问题,不过跟第二个问题相关,就一起说了,就是设置超时时间,有些对于不是锁的 redis 缓存操作可以是先设置好值,然后在设置过期时间,那么这就又有上面说到的不是原子性的问题,那么就需要在同一条指令里把超时时间也设置了,幸好 redis 提供了这种支持
SET resource_name my_random_value NX PX 30000
-这里借鉴一下解释下,resource_name就是 key,代表要锁住的东西,my_random_value就是识别我这个线程的,NX代表只有在不存在的时候才设置,然后PX 30000表示超时时间是 30秒自动过期
PS:记录下我原先有的一个误区,是不是要用 key 来区分加锁的线程,这样只有一个用处,就是自身线程可以识别是否是自己加的锁,但是最大的问题是别的线程不知道,其实这个用户的出发点是我在担心前面提过的一个问题,就是当 gc 停顿后,我要去判断当前的这个锁是否是我加的,还有就是当释放锁的时候,如果保证不会错误释放了其他线程加的锁,但是这样附带很多其他问题,最大的就是其他线程怎么知道能不能加这个锁。
-当线程在锁过期之前就处理完了业务逻辑,那就可以提前释放这个锁,那么提前释放要怎么操作,直接del key显然是不行的,因为这样就是我前面想用线程随机值加资源名作为锁的初衷,我不能去释放别的线程加的锁,那么我要怎么办呢,先 get 一下看是不是我的?那又变成非原子的操作了,幸好redis 也考虑到了这个问题,给了lua 脚本来操作这种
if redis.call("get",KEYS[1]) == ARGV[1] then
- return redis.call("del",KEYS[1])
-else
- return 0
-end
-这里的KEYS[1]就是前面加锁的resource_name,ARGV[1]就是线程的随机值my_random_value
前面说的其实是单节点 redis 作为分布式锁的情况,那么当我们的 redis 有多节点的情况呢,如果多节点下处于加锁或者解锁或者锁有效情况下redis 的某个节点宕掉了怎么办,这里就有一些需要思考的地方,是否单独搞一个单节点的 redis作为分布式锁专用的,但是如果这个单节点的挂了呢,还有就是成本问题,所以我们需要一个多节点的分布式锁方案
这里就引出了开头说到的redlock,这个可是 redis的作者写的, 他的加锁过程是分以下几步去做这个事情
Tailscale 和 Headscale 的方式,就想着试试看,没想到一开始就踩了几个比较莫名其妙的坑。Error initializing error="failed to read or create private key: failed to save private key to disk: open /etc/headscale/private.key: read-only file system"
-其实一开始看到这个我都有点懵了,咋回事呢,read-only file system一般有可能是文件系统出问题了,不可写入,需要重启或者修改挂载方式,被这个错误的错误日志给误导了,后面才知道是配置文件,在另一个教程中也有个类似的回复,一开始没注意,其实就是同一个问题。
默认的配置文件是这样的
---
-# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:
-#
-# - `/etc/headscale`
-# - `~/.headscale`
-# - current working directory
-
-# The url clients will connect to.
-# Typically this will be a domain like:
-#
-# https://myheadscale.example.com:443
-#
-server_url: http://127.0.0.1:8080
+ Number of 1 Bits
+ /2015/03/11/Number-Of-1-Bits/
+ Number of 1 Bits Write a function that takes an unsigned integer and returns the number of ’1’ bits it has (also known as the Hamming weight). For example, the 32-bit integer ‘11’ has binary representation 00000000000000000000000000001011, so the function should return 3.
-# Address to listen to / bind to on the server
-#
-# For production:
-# listen_addr: 0.0.0.0:8080
-listen_addr: 127.0.0.1:8080
+分析
从1位到2位到4位逐步的交换
+
+code
int hammingWeight(uint32_t n) {
+ const uint32_t m1 = 0x55555555; //binary: 0101...
+ const uint32_t m2 = 0x33333333; //binary: 00110011..
+ const uint32_t m4 = 0x0f0f0f0f; //binary: 4 zeros, 4 ones ...
+ const uint32_t m8 = 0x00ff00ff; //binary: 8 zeros, 8 ones ...
+ const uint32_t m16 = 0x0000ffff; //binary: 16 zeros, 16 ones ...
+
+ n = (n & m1 ) + ((n >> 1) & m1 ); //put count of each 2 bits into those 2 bits
+ n = (n & m2 ) + ((n >> 2) & m2 ); //put count of each 4 bits into those 4 bits
+ n = (n & m4 ) + ((n >> 4) & m4 ); //put count of each 8 bits into those 8 bits
+ n = (n & m8 ) + ((n >> 8) & m8 ); //put count of each 16 bits into those 16 bits
+ n = (n & m16) + ((n >> 16) & m16); //put count of each 32 bits into those 32 bits
+ return n;
-# Address to listen to /metrics, you may want
-# to keep this endpoint private to your internal
-# network
-#
-metrics_listen_addr: 127.0.0.1:9090
+}
]]>
+
+ leetcode
+
+
+ leetcode
+ c++
+
+ Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.
+ +For example:
Given the below binary tree and sum = 22,
5
+ / \
+ 4 8
+ / / \
+ 11 13 4
+ / \ \
+7 2 1
+return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.
+using simple deep first search
+/*
+ Definition for binary tree
+ struct TreeNode {
+ int val;
+ TreeNode *left;
+ TreeNode *right;
+ TreeNode(int x) : val(x), left(NULL), right(NULL)}
+ };
+ */
+class Solution {
+public:
+ bool deep_first_search(TreeNode *node, int sum, int curSum)
+ {
+ if (node == NULL)
+ return false;
+
+ if (node->left == NULL && node->right == NULL)
+ return curSum + node->val == sum;
+
+ return deep_first_search(node->left, sum, curSum + node->val) || deep_first_search(node->right, sum, curSum + node->val);
+ }
+
+ bool hasPathSum(TreeNode *root, int sum) {
+ // Start typing your C/C++ solution below
+ // DO NOT write int main() function
+ return deep_first_search(root, sum, 0);
+ }
+};
+]]>redis 分布式锁 redlock 的实现,简单记录下,
+原先我对 redis 锁的概念就是加锁使用 setnx,解锁使用 lua 脚本,但是 setnx 具体是啥,lua 脚本是啥不是很清楚
首先简单思考下这个问题,首先为啥不是先 get 一下 key 存不存在,然后再 set 一个 key value,因为加锁这个操作我们是要保证两点,一个是不能中途被打断,也就是说要原子性,如果是先 get 一下 key,如果不存在再 set 值的话,那就不是原子操作了;第二个是可不可以直接 set 值呢,显然不行,锁要保证唯一性,有且只能有一个线程或者其他应用单位获得该锁,正好 setnx 给了我们这种原子命令
然后是 setnx 的键和值分别是啥,键比较容易想到是要锁住的资源,比如 user_id, 这里有个我自己之前比较容易陷进去的误区,但是这个误区后
面再说,这里其实是把user_id 作为要锁住的资源,在我获得锁的时候别的线程不允许操作,以此保证业务的正确性,不会被多个线程同时修改,确定了键,再来看看值是啥,其实原先我认为值是啥都没关系,我只要锁住了,光键就够我用了,但是考虑下多个线程的问题,如果我这个线程加了锁,然后我因为 gc 停顿等原因卡死了,这个时候redis 的锁或者说就是 redis 的缓存已经过期了,这时候另一个线程获得锁成功,然后我这个线程又活过来了,然后我就仍然认为我拿着锁,我去对数据进行修改或者释放锁,是不是就出现问题了,所以是不是我们还需要一个东西来区分这个锁是哪个线程加的,所以我们可以将值设置成为一个线程独有识别的值,至少在相对长的一段时间内不会重复。
上面其实还有两个问题,一个是当 gc 超时时,我这个线程如何知道我手里的锁已经过期了,一种方法是我在加好锁之后就维护一个超时时间,这里其实还有个问题,不过跟第二个问题相关,就一起说了,就是设置超时时间,有些对于不是锁的 redis 缓存操作可以是先设置好值,然后在设置过期时间,那么这就又有上面说到的不是原子性的问题,那么就需要在同一条指令里把超时时间也设置了,幸好 redis 提供了这种支持
SET resource_name my_random_value NX PX 30000
+这里借鉴一下解释下,resource_name就是 key,代表要锁住的东西,my_random_value就是识别我这个线程的,NX代表只有在不存在的时候才设置,然后PX 30000表示超时时间是 30秒自动过期
PS:记录下我原先有的一个误区,是不是要用 key 来区分加锁的线程,这样只有一个用处,就是自身线程可以识别是否是自己加的锁,但是最大的问题是别的线程不知道,其实这个用户的出发点是我在担心前面提过的一个问题,就是当 gc 停顿后,我要去判断当前的这个锁是否是我加的,还有就是当释放锁的时候,如果保证不会错误释放了其他线程加的锁,但是这样附带很多其他问题,最大的就是其他线程怎么知道能不能加这个锁。
+当线程在锁过期之前就处理完了业务逻辑,那就可以提前释放这个锁,那么提前释放要怎么操作,直接del key显然是不行的,因为这样就是我前面想用线程随机值加资源名作为锁的初衷,我不能去释放别的线程加的锁,那么我要怎么办呢,先 get 一下看是不是我的?那又变成非原子的操作了,幸好redis 也考虑到了这个问题,给了lua 脚本来操作这种
if redis.call("get",KEYS[1]) == ARGV[1] then
+ return redis.call("del",KEYS[1])
+else
+ return 0
+end
+这里的KEYS[1]就是前面加锁的resource_name,ARGV[1]就是线程的随机值my_random_value
前面说的其实是单节点 redis 作为分布式锁的情况,那么当我们的 redis 有多节点的情况呢,如果多节点下处于加锁或者解锁或者锁有效情况下redis 的某个节点宕掉了怎么办,这里就有一些需要思考的地方,是否单独搞一个单节点的 redis作为分布式锁专用的,但是如果这个单节点的挂了呢,还有就是成本问题,所以我们需要一个多节点的分布式锁方案
这里就引出了开头说到的redlock,这个可是 redis的作者写的, 他的加锁过程是分以下几步去做这个事情
For example, given input 43261596 (represented in binary as 00000010100101000001111010011100), return 964176192 (represented in binary as 00111001011110000010100101000000).
+ +Follow up:
If this function is called many times, how would you optimize it?
class Solution {
+public:
+ uint32_t reverseBits(uint32_t n) {
+ n = ((n >> 1) & 0x55555555) | ((n & 0x55555555) << 1);
+ n = ((n >> 2) & 0x33333333) | ((n & 0x33333333) << 2);
+ n = ((n >> 4) & 0x0f0f0f0f) | ((n & 0x0f0f0f0f) << 4);
+ n = ((n >> 8) & 0x00ff00ff) | ((n & 0x00ff00ff) << 8);
+ n = ((n >> 16) & 0x0000ffff) | ((n & 0x0000ffff) << 16);
+ return n;
+ }
+};
+]]>Example1: x = 123, return 321
Example2: x = -123, return -321
Have you thought about this?
Here are some good questions to ask before coding. Bonus points for you if you have already thought through this!
If the integer’s last digit is 0, what should the output be? ie, cases such as 10, 100.
+Did you notice that the reversed integer might overflow? Assume the input is a 32-bit integer, then the reverse of 1000000003 overflows. How should you handle such cases?
+For the purpose of this problem, assume that your function returns 0 when the reversed integer overflows.
+class Solution {
+public:
+ int reverse(int x) {
-# Address to listen for gRPC.
-# gRPC is used for controlling a headscale server
-# remotely with the CLI
-# Note: Remote access _only_ works if you have
-# valid certificates.
-#
-# For production:
-# grpc_listen_addr: 0.0.0.0:50443
-grpc_listen_addr: 127.0.0.1:50443
+ int max = 1 << 31 - 1;
+ int ret = 0;
+ max = (max - 1) * 2 + 1;
+ int min = 1 << 31;
+ if(x < 0)
+ while(x != 0){
+ if(ret < (min - x % 10) / 10)
+ return 0;
+ ret = ret * 10 + x % 10;
+ x = x / 10;
+ }
+ else
+ while(x != 0){
+ if(ret > (max -x % 10) / 10)
+ return 0;
+ ret = ret * 10 + x % 10;
+ x = x / 10;
+ }
+ return ret;
+ }
+};
+]]>Given an array of integers, find two numbers such that they add up to a specific target number.
+The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.
+ +You may assume that each input would have exactly one solution.
+Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
struct Node
+{
+ int num, pos;
+};
+bool cmp(Node a, Node b)
+{
+ return a.num < b.num;
+}
+class Solution {
+public:
+ vector<int> twoSum(vector<int> &numbers, int target) {
+ // Start typing your C/C++ solution below
+ // DO NOT write int main() function
+ vector<int> result;
+ vector<Node> array;
+ for (int i = 0; i < numbers.size(); i++)
+ {
+ Node temp;
+ temp.num = numbers[i];
+ temp.pos = i;
+ array.push_back(temp);
+ }
-# Allow the gRPC admin interface to run in INSECURE
-# mode. This is not recommended as the traffic will
-# be unencrypted. Only enable if you know what you
-# are doing.
-grpc_allow_insecure: false
-
-# Private key used to encrypt the traffic between headscale
-# and Tailscale clients.
-# The private key file will be autogenerated if it's missing.
-#
-# For production:
-# /var/lib/headscale/private.key
-private_key_path: ./private.key
-
-# The Noise section includes specific configuration for the
-# TS2021 Noise protocol
-noise:
- # The Noise private key is used to encrypt the
- # traffic between headscale and Tailscale clients when
- # using the new Noise-based protocol. It must be different
- # from the legacy private key.
- #
- # For production:
- # private_key_path: /var/lib/headscale/noise_private.key
- private_key_path: ./noise_private.key
-
-# List of IP prefixes to allocate tailaddresses from.
-# Each prefix consists of either an IPv4 or IPv6 address,
-# and the associated prefix length, delimited by a slash.
-# While this looks like it can take arbitrary values, it
-# needs to be within IP ranges supported by the Tailscale
-# client.
-# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
-# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
-ip_prefixes:
- - fd7a:115c:a1e0::/48
- - 100.64.0.0/10
-
-# DERP is a relay system that Tailscale uses when a direct
-# connection cannot be established.
-# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
-#
-# headscale needs a list of DERP servers that can be presented
-# to the clients.
-derp:
- server:
- # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
- # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
- enabled: false
-
- # Region ID to use for the embedded DERP server.
- # The local DERP prevails if the region ID collides with other region ID coming from
- # the regular DERP config.
- region_id: 999
-
- # Region code and name are displayed in the Tailscale UI to identify a DERP region
- region_code: "headscale"
- region_name: "Headscale Embedded DERP"
-
- # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
- # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
- #
- # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
- stun_listen_addr: "0.0.0.0:3478"
+ sort(array.begin(), array.end(), cmp);
+ for (int i = 0, j = array.size() - 1; i != j;)
+ {
+ int sum = array[i].num + array[j].num;
+ if (sum == target)
+ {
+ if (array[i].pos < array[j].pos)
+ {
+ result.push_back(array[i].pos + 1);
+ result.push_back(array[j].pos + 1);
+ } else
+ {
+ result.push_back(array[j].pos + 1);
+ result.push_back(array[i].pos + 1);
+ }
+ break;
+ } else if (sum < target)
+ {
+ i++;
+ } else if (sum > target)
+ {
+ j--;
+ }
+ }
+ return result;
+ }
+};
- # List of externally available DERP maps encoded in JSON
- urls:
- - https://controlplane.tailscale.com/derpmap/default
+sort the array, then test from head and end, until catch the right answer
+]]>ambari是一个大数据平台的管理工具,包含了hadoop, yarn, hive, hbase, spark等大数据的基础架构和工具,简化了数据平台的搭建,之前只是在同事搭建好平台后的一些使用,这次有机会从头开始用ambari来搭建一个测试的数据平台,过程中也踩到不少坑,简单记录下。
/etc/yum.repos.d/路径下,然后yum update && yum install ambari-server安装即可,如果有条件就用proxychains走下代理。ambari-server setup做一些初始化设置,其中包含了JDK路径的设置,数据库设置,设置好就OK了,然后执行ambari-server start启动服务,这里有个小插曲,因为ambari-server涉及到这么多服务,所以管理控制监控之类的模块是必不可少的,这部分可以在ambari-server的web ui界面安装,也可以命令行提前安装,这部分被称为HDF Management Pack,运行ambari-server install-mpack \ --mpack=http://public-repo-1.hortonworks.com/HDF/centos7/2.x/updates/2.1.4.0/tars/hdf_ambari_mp/hdf-ambari-mpack-2.1.4.0-5.tar.gz \ --purge \ --verboseambari-serverA binary watch has 4 LEDs on the top which represent the hours (0-11), and the 6 LEDs on the bottom represent the minutes (0-59).
+Each LED represents a zero or one, with the least significant bit on the right.
+
For example, the above binary watch reads “3:25”.
+Given a non-negative integer n which represents the number of LEDs that are currently on, return all possible times the watch could represent.
+Input: n = 1
+Return: ["1:00", "2:00", "4:00", "8:00", "0:01", "0:02", "0:04", "0:08", "0:16", "0:32"]
+又是参(chao)考(xi)别人的代码,嗯,就是这么不要脸,链接
+class Solution {
+public:
+ vector<string> readBinaryWatch(int num) {
+ vector<string> res;
+ for (int h = 0; h < 12; ++h) {
+ for (int m = 0; m < 60; ++m) {
+ if (bitset<10>((h << 6) + m).count() == num) {
+ res.push_back(to_string(h) + (m < 10 ? ":0" : ":") + to_string(m));
+ }
+ }
+ }
+ return res;
+ }
+};]]>基于docker搭了个mysql集群,稍微记一下,
首先是新建mysql主库容
docker run -d -e MYSQL_ROOT_PASSWORD=admin --name mysql-master -p 3307:3306 mysql:latest-d表示容器运行在后台,-e表示设置环境变量,即MYSQL_ROOT_PASSWORD=admin,设置了mysql的root密码,--name表示容器名,-p表示端口映射,将内部mysql:3306映射为外部的3307,最后的mysql:latest表示镜像名
此外还可以用-v /local_path/my-master.cnf:/etc/mysql/my.cnf来映射配置文件
然后同理启动从库docker run -d -e MYSQL_ROOT_PASSWORD=admin --name mysql-slave -p 3308:3306 mysql:latest
然后进入主库改下配置文件docker-enter mysql-master如果无法进入就用docker ps -a看下容器是否在正常运行,如果status显示
未正常运行,则用docker logs mysql-master看下日志哪里出错了。
进入容器后,我这边使用的镜像的mysqld配置文件是在/etc/mysql下面,这个最新版本的mysql的配置文件包含
三部分,/etc/mysql/my.cnf和/etc/mysql/conf.d/mysql.cnf,还有/etc/mysql/mysql.conf.d/mysqld.cnf
这里需要改的是最后一个,加上
log-bin = mysql-bin
+server_id = 1
+保存后退出容器重启主库容器,然后进入从库更改相同文件,
+log-bin = mysql-bin
+server_id = 2
+同样退出重启容器,然后是配置主从,首先进入主库,用命令mysql -u root -pxxxx进入mysql,然后赋予一个同步
权限GRANT REPLICATION SLAVE ON *.* to 'backup'@'%' identified by '123456';还是同样说明下,ON *.*表示了数
据库全部的权限,如果要指定数据库/表的话可以使用类似testDb/testTable,然后是'backup'@'%'表示给予同步
权限的用户名及其主机ip,%表示不限制ip,当然如果有防火墙的话还是会有限制的,最后的identified by '123456'
表示同步用户的密码,然后就查看下主库的状态信息show master status,如下图:![9G5FE[9%@7%G(B`Q7]E)5@R.png](https://ooo.0o0.ooo/2016/08/10/57aac43029559.png)
把file跟position记下来,然后再开一个terminal,进入从库容器,登陆mysql,然后设置主库
change master to
+master_host='xxx.xxx.xxx.xxx', //如果主从库的容器都在同一个宿主机上,这里的ip是docker容器的ip
+master_user='backup', //就是上面的赋予权限的用户
+master_password='123456',
+master_log_file='mysql-bin.000004', //主库中查看到的file
+master_log_pos=312, //主库中查看到的position
+master_port=3306; //如果是同一宿主机,这里使用真实的3306端口,3308及主库的3307是给外部连接使用的
+通过docker-ip mysql-master可以查看容器的ip
这里有一点是要注意的,也是我踩的坑,就是如果是同一宿主机下两个mysql容器互联,我这里只能通过docker-ip和真实
的3306端口能够连接成功。
本文参考了这位同学的文章
docker最开始是之前在某位大佬的博客看到的,看上去有点神奇,感觉是一种轻量级的虚拟机,但是能做的事情好像差不多,那时候是在Ubuntu系统的vps里起一个Ubuntu的docker,然后在里面装个nginx,配置端口映射就可以访问了,后来也草草写过一篇使用docker搭建mysql集群,但是最近看了下好像是因为装docker的大佬做了一些别名还是什么操作,导致里面用的操作都不具有普遍性,而且主要是把搭的过程写了下,属于囫囵吞枣,没理解docker是干啥的,为啥用docker,就是操作了下,这几天借着搭phabricator的过程,把一些原来不理解,或者原来理解错误的地方重新理一下。
+之前写的 mysql 集群,一主二备,这种架构在很多小型应用里都是这么配置的,而且一般是直接在三台 vps 里启动三个 mysql 实例,但是如果换成 docker 会有什么好处呢,其实就是方便部署,比如其中一台备库挂了,我要加一台,或者说备库的 qps 太高了,需要再加一个,如果要在 vps 上搭建的话,首先要买一台机器,等初始化,然后在上面修改源,更新,装 mysql ,然后配置主从,可能还要处理防火墙等等,如果把这些打包成一个 docker 镜像,并且放在自己的 docker registry,那就直接run 一下就可以了;还有比如在公司要给一个新同学整一套开发测试环境,以 Java 开发为例,要装 git,maven,jdk,配置 maven settings 和各种 rc,整合在一个镜像里的话,就会很方便了;再比如微服务的水平扩展。
+但是为啥 docker 会有这种优势,听起来好像虚拟机也可以干这个事,但是虚拟机动辄上 G,而且需要 VMware,virtual box 等支持,不适合在Linux服务器环境使用,而且占用资源也会非常大。说得这么好,那么 docker 是啥呢
+docker 主要使用 Linux 中已经存在的两种技术的一个整合升级,一个是 namespace,一个是cgroups,相比于虚拟机需要完整虚拟出一个操作系统运行基础,docker 基于宿主机内核,通过 namespace 和 cgroups 分隔进程,理念就是提供一个隔离的最小化运行依赖,这样子相对于虚拟机就有了巨大的便利性,具体的 namespace 和 cgroups 就先不展开讲,可以参考耗子叔的文章
+那么我们先安装下 docker,参考官方的教程,安装,我的系统是 ubuntu 的,就贴了 ubuntu 的链接,用其他系统的可以找到对应的系统文档安装,安装完了的话看看 docker 的信息
+sudo docker info
- # Locally available DERP map files encoded in YAML
- #
- # This option is mostly interesting for people hosting
- # their own DERP servers:
- # https://tailscale.com/kb/1118/custom-derp-servers/
- #
- # paths:
- # - /etc/headscale/derp-example.yaml
- paths: []
+输出以下信息
然后再来运行个 hello world 呗,
+sudo docker run hello-world
- # If enabled, a worker will be set up to periodically
- # refresh the given sources and update the derpmap
- # will be set up.
- auto_update_enabled: true
+输出了这些
看看这个运行命令是怎么用的,一般都会看到这样子的,sudo docker run -it ubuntu bash, 前面的 docker run 反正就是运行一个容器的意思,-it是啥呢,还有这个什么 ubuntu bash,来看看docker run`的命令帮助信息
-i, --interactive Keep STDIN open even if not attached
- # How often should we check for DERP updates?
- update_frequency: 24h
+就是要有输入,我们运行的时候能输入
+-t, --tty Allocate a pseudo-TTY
-# Disables the automatic check for headscale updates on startup
-disable_check_updates: false
+要有个虚拟终端,
+Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
-# Time before an inactive ephemeral node is deleted?
-ephemeral_node_inactivity_timeout: 30m
+Run a command in a new container
-# Period to check for node updates within the tailnet. A value too low will severely affect
-# CPU consumption of Headscale. A value too high (over 60s) will cause problems
-# for the nodes, as they won't get updates or keep alive messages frequently enough.
-# In case of doubts, do not touch the default 10s.
-node_update_check_interval: 10s
+上面说的-it 就是这里的 options,后面那个 ubuntu 就是 image 辣,image 是啥呢
+Docker 把应用程序及其依赖,打包在 image 文件里面,可以把它理解成为类似于虚拟机的镜像或者运行一个进程的代码,跑起来了的叫docker 容器或者进程,比如我们将要运行的docker run -it ubuntu bash的ubuntu 就是个 ubuntu 容器的镜像,将这个镜像运行起来后,我们可以进入容器像使用 ubuntu 一样使用它,来看下我们的镜像,使用sudo docker image ls就能列出我们宿主机上的 docker 镜像了

一个 ubuntu 镜像才 64MB,非常小巧,然后是后面的bash,我通过交互式启动了一个 ubuntu 容器,然后在这个启动的容器里运行了 bash 命令,这样就可以在容器里玩一下了

只有刚才运行容器的 bash 进程和我刚执行的 ps,这里有个可以注意下的,bash 这个进程的 pid 是 1,其实这里就用到了 linux 中的PID Namespace,容器会隔离出一个 pid 的名字空间,这里面的进程跟外部的 pid 命名独立
+sudo docker ps -a
-# SQLite config
-db_type: sqlite3
+
这个应该是比较常用的,因为比如是一个微服务容器,有时候就像看下运行状态,日志啥的
+sudo docker exec -it [containerID] bash
-# For production:
-# db_path: /var/lib/headscale/db.sqlite
-db_path: ./db.sqlite
+
sudo docker logs [containerID]
-# # Postgres config
-# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
-# db_type: postgres
-# db_host: localhost
-# db_port: 5432
-# db_name: headscale
-# db_user: foo
-# db_pass: bar
-
-# If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
-# in the 'db_ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
-# db_ssl: false
-
-### TLS configuration
-#
-## Let's encrypt / ACME
-#
-# headscale supports automatically requesting and setting up
-# TLS for a domain with Let's Encrypt.
-#
-# URL to ACME directory
-acme_url: https://acme-v02.api.letsencrypt.org/directory
-
-# Email to register with ACME provider
-acme_email: ""
-
-# Domain name to request a TLS certificate for:
-tls_letsencrypt_hostname: ""
-
-# Path to store certificates and metadata needed by
-# letsencrypt
-# For production:
-# tls_letsencrypt_cache_dir: /var/lib/headscale/cache
-tls_letsencrypt_cache_dir: ./cache
-
-# Type of ACME challenge to use, currently supported types:
-# HTTP-01 or TLS-ALPN-01
-# See [docs/tls.md](docs/tls.md) for more information
-tls_letsencrypt_challenge_type: HTTP-01
-# When HTTP-01 challenge is chosen, letsencrypt must set up a
-# verification endpoint, and it will be listening on:
-# :http = port 80
-tls_letsencrypt_listen: ":http"
-
-## Use already defined certificates:
-tls_cert_path: ""
-tls_key_path: ""
-
-log:
- # Output formatting for logs: text or json
- format: text
- level: info
-
-# Path to a file containg ACL policies.
-# ACLs can be defined as YAML or HUJSON.
-# https://tailscale.com/kb/1018/acls/
-acl_policy_path: ""
-
-## DNS
-#
-# headscale supports Tailscale's DNS configuration and MagicDNS.
-# Please have a look to their KB to better understand the concepts:
-#
-# - https://tailscale.com/kb/1054/dns/
-# - https://tailscale.com/kb/1081/magicdns/
-# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
-#
-dns_config:
- # Whether to prefer using Headscale provided DNS or use local.
- override_local_dns: true
-
- # List of DNS servers to expose to clients.
- nameservers:
- - 1.1.1.1
-
- # NextDNS (see https://tailscale.com/kb/1218/nextdns/).
- # "abc123" is example NextDNS ID, replace with yours.
- #
- # With metadata sharing:
- # nameservers:
- # - https://dns.nextdns.io/abc123
- #
- # Without metadata sharing:
- # nameservers:
- # - 2a07:a8c0::ab:c123
- # - 2a07:a8c1::ab:c123
-
- # Split DNS (see https://tailscale.com/kb/1054/dns/),
- # list of search domains and the DNS to query for each one.
- #
- # restricted_nameservers:
- # foo.bar.com:
- # - 1.1.1.1
- # darp.headscale.net:
- # - 1.1.1.1
- # - 8.8.8.8
-
- # Search domains to inject.
- domains: []
-
- # Extra DNS records
- # so far only A-records are supported (on the tailscale side)
- # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
- # extra_records:
- # - name: "grafana.myvpn.example.com"
- # type: "A"
- # value: "100.64.0.3"
- #
- # # you can also put it in one line
- # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
-
- # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
- # Only works if there is at least a nameserver defined.
- magic_dns: true
-
- # Defines the base domain to create the hostnames for MagicDNS.
- # `base_domain` must be a FQDNs, without the trailing dot.
- # The FQDN of the hosts will be
- # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_).
- base_domain: example.com
-
-# Unix socket used for the CLI to connect without authentication
-# Note: for production you will want to set this to something like:
-# unix_socket: /var/run/headscale.sock
-unix_socket: ./headscale.sock
-unix_socket_permission: "0770"
-#
-# headscale supports experimental OpenID connect support,
-# it is still being tested and might have some bugs, please
-# help us test it.
-# OpenID Connect
-# oidc:
-# only_start_if_oidc_is_available: true
-# issuer: "https://your-oidc.issuer.com/path"
-# client_id: "your-oidc-client-id"
-# client_secret: "your-oidc-client-secret"
-# # Alternatively, set `client_secret_path` to read the secret from the file.
-# # It resolves environment variables, making integration to systemd's
-# # `LoadCredential` straightforward:
-# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
-# # client_secret and client_secret_path are mutually exclusive.
-#
-# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
-# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
-#
-# scope: ["openid", "profile", "email", "custom"]
-# extra_params:
-# domain_hint: example.com
-#
-# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
-# authentication request will be rejected.
-#
-# allowed_domains:
-# - example.com
-# Groups from keycloak have a leading '/'
-# allowed_groups:
-# - /headscale
-# allowed_users:
-# - alice@example.com
-#
-# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
-# This will transform `first-name.last-name@example.com` to the user `first-name.last-name`
-# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
-# user: `first-name.last-name.example.com`
-#
-# strip_email_domain: true
-
-# Logtail configuration
-# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel
-# to instruct tailscale nodes to log their activity to a remote server.
-logtail:
- # Enable logtail for this headscales clients.
- # As there is currently no support for overriding the log server in headscale, this is
- # disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
- enabled: false
-
-# Enabling this option makes devices prefer a random port for WireGuard traffic over the
-# default static port 41641. This option is intended as a workaround for some buggy
-# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
-randomize_client_port: false
-
-问题就是出在几个文件路径的配置,默认都是当前目录,也就是headscale的可执行文件所在目录,需要按它配置说明中的生产配置进行修改
-# For production:
-# /var/lib/headscale/private.key
-private_key_path: /var/lib/headscale/private.key
-直接改成绝对路径就好了,还有两个文件路径
另一个也是个秘钥的路径问题
noise:
- # The Noise private key is used to encrypt the
- # traffic between headscale and Tailscale clients when
- # using the new Noise-based protocol. It must be different
- # from the legacy private key.
- #
- # For production:
- # private_key_path: /var/lib/headscale/noise_private.key
- private_key_path: /var/lib/headscale/noise_private.key
-这个问题也是一种误导,
错误信息是
Error initializing error="unable to open database file: out of memory (14)"
-这就是个文件,内存也完全没有被占满的迹象,原来也是文件路径的问题
-# For production:
-# db_path: /var/lib/headscale/db.sqlite
-db_path: /var/lib/headscale/db.sqlite
-都改成绝对路径就可以了,然后这里还有个就是要对/var/lib/headscale/和/etc/headscale/等路径赋予headscale用户权限,有时候对这类问题的排查真的蛮头疼,日志报错都不是真实的错误信息,开源项目对这些错误的提示真的也需要优化,后续的譬如mac也加入节点等后面再开篇讲
Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.
- -For example:
Given the below binary tree and sum = 22,
5
- / \
- 4 8
- / / \
- 11 13 4
- / \ \
-7 2 1
-return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.
-using simple deep first search
-/*
- Definition for binary tree
- struct TreeNode {
- int val;
- TreeNode *left;
- TreeNode *right;
- TreeNode(int x) : val(x), left(NULL), right(NULL)}
- };
- */
-class Solution {
-public:
- bool deep_first_search(TreeNode *node, int sum, int curSum)
- {
- if (node == NULL)
- return false;
-
- if (node->left == NULL && node->right == NULL)
- return curSum + node->val == sum;
-
- return deep_first_search(node->left, sum, curSum + node->val) || deep_first_search(node->right, sum, curSum + node->val);
- }
-
- bool hasPathSum(TreeNode *root, int sum) {
- // Start typing your C/C++ solution below
- // DO NOT write int main() function
- return deep_first_search(root, sum, 0);
- }
-};
+我在运行容器的终端里胡乱输入点啥,然后通过上面的命令就可以看到啦
+

For example, given input 43261596 (represented in binary as 00000010100101000001111010011100), return 964176192 (represented in binary as 00111001011110000010100101000000).
- -Follow up:
If this function is called many times, how would you optimize it?
class Solution {
-public:
- uint32_t reverseBits(uint32_t n) {
- n = ((n >> 1) & 0x55555555) | ((n & 0x55555555) << 1);
- n = ((n >> 2) & 0x33333333) | ((n & 0x33333333) << 2);
- n = ((n >> 4) & 0x0f0f0f0f) | ((n & 0x0f0f0f0f) << 4);
- n = ((n >> 8) & 0x00ff00ff) | ((n & 0x00ff00ff) << 8);
- n = ((n >> 16) & 0x0000ffff) | ((n & 0x0000ffff) << 16);
- return n;
- }
-};
-]]>给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
--输入:nums1 = [1,3], nums2 = [2]
-
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
--输入:nums1 = [1,2], nums2 = [3,4]
-
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
这个题也是我随机出来的,之前都是随机到 easy 的,而且是序号这么靠前的,然后翻一下,之前应该是用 C++做过的,具体的方法其实可以从他的算法时间复杂度要求看出来,大概率是要二分法这种,后面就结合代码来讲了
-public double findMedianSortedArrays(int[] nums1, int[] nums2) {
- int n1 = nums1.length;
- int n2 = nums2.length;
- if (n1 > n2) {
- return findMedianSortedArrays(nums2, nums1);
- }
-
- // 找到两个数组的中点下标
- int k = (n1 + n2 + 1 ) / 2;
- // 使用一个类似于二分法的查找方法
- // 起始值就是 num1 的头跟尾
- int left = 0;
- int right = n1;
- while (left < right) {
- // m1 表示我取的是 nums1 的中点,即二分法的方式
- int m1 = left + (right - left) / 2;
- // *** 这里是重点,因为这个问题也可以转换成找成 n1 + n2 那么多个数中的前 (n1 + n2 + 1) / 2 个
- // *** 因为两个数组都是排好序的,那么我从 num1 中取了 m1 个,从 num2 中就是去 k - m1 个
- // *** 但是不知道取出来大小是否正好是整体排序的第 (n1 + n2 + 1) / 2 个,所以需要二分法上下对比
- int m2 = k - m1;
- // 如果 nums1[m1] 小,那我在第一个数组 nums1 的二分查找就要把左端点改成前一次的中点 + 1 (不然就进死循环了
- if (nums1[m1] < nums2[m2 - 1]) {
- left = m1 + 1;
- } else {
- right = m1;
- }
- }
-
- // 因为对比后其实我们只是拿到了一个位置,具体哪个是第 k 个就需要继续判断
- int m1 = left;
- int m2 = k - left;
- // 如果 m1 或者 m2 有小于等于 0 的,那这个值可以先抛弃
- // m1 如果等于 0,就是 num1[0] 都比 nums2 中所有值都要大
- // m2 等于 0 的话 刚好相反
- // 可以这么推断,当其中一个是 0 的时候那么另一个 mx 值肯定是> 0 的,那么就是取的对应的这个下标的值
- int c1 = Math.max( m1 <= 0 ? Integer.MIN_VALUE : nums1[m1 - 1] , m2 <= 0 ? Integer.MIN_VALUE : nums2[m2 - 1]);
- // 如果两个数组的元素数量和是奇数,那就直接可以返回了,因为 m1 + m2 就是 k, 如果是一个数组,那这个元素其实就是 nums[k - 1]
- // 如果 m1 或者 m2 是 0,那另一个就是 k,取 mx - 1的下标就等于是 k - 1
- // 如果都不是 0,那就是取的了 nums1[m1 - 1] 与 nums2[m2 - 1]中的较大者,如果取得是后者,那么也就是 m1 + m2 - 1 的下标就是 k - 1
- if ((n1 + n2) % 2 == 1) {
- return c1;
- }
- // 如果是偶数个,那还要取两个数组后面的较小者,然后求平均值
- int c2 = Math.min(m1 >= n1 ? Integer.MAX_VALUE : nums1[m1], m2 >= n2 ? Integer.MAX_VALUE : nums2[m2]);
- return (c1 + c2) / 2.0;
- }
-前面考虑的方法还是比较繁琐,考虑了两个数组的各种交叉情况,后面这个参考了一些网上的解法,代码比较简洁,但是可能不容易一下子就搞明白,所以配合了比较多的注释。
-]]>Example1: x = 123, return 321
Example2: x = -123, return -321
Have you thought about this?
Here are some good questions to ask before coding. Bonus points for you if you have already thought through this!
If the integer’s last digit is 0, what should the output be? ie, cases such as 10, 100.
-Did you notice that the reversed integer might overflow? Assume the input is a 32-bit integer, then the reverse of 1000000003 overflows. How should you handle such cases?
-For the purpose of this problem, assume that your function returns 0 when the reversed integer overflows.
-class Solution {
-public:
- int reverse(int x) {
-
- int max = 1 << 31 - 1;
- int ret = 0;
- max = (max - 1) * 2 + 1;
- int min = 1 << 31;
- if(x < 0)
- while(x != 0){
- if(ret < (min - x % 10) / 10)
- return 0;
- ret = ret * 10 + x % 10;
- x = x / 10;
- }
- else
- while(x != 0){
- if(ret > (max -x % 10) / 10)
- return 0;
- ret = ret * 10 + x % 10;
- x = x / 10;
- }
- return ret;
- }
-};
-]]>ambari是一个大数据平台的管理工具,包含了hadoop, yarn, hive, hbase, spark等大数据的基础架构和工具,简化了数据平台的搭建,之前只是在同事搭建好平台后的一些使用,这次有机会从头开始用ambari来搭建一个测试的数据平台,过程中也踩到不少坑,简单记录下。
/etc/yum.repos.d/路径下,然后yum update && yum install ambari-server安装即可,如果有条件就用proxychains走下代理。ambari-server setup做一些初始化设置,其中包含了JDK路径的设置,数据库设置,设置好就OK了,然后执行ambari-server start启动服务,这里有个小插曲,因为ambari-server涉及到这么多服务,所以管理控制监控之类的模块是必不可少的,这部分可以在ambari-server的web ui界面安装,也可以命令行提前安装,这部分被称为HDF Management Pack,运行ambari-server install-mpack \ --mpack=http://public-repo-1.hortonworks.com/HDF/centos7/2.x/updates/2.1.4.0/tars/hdf_ambari_mp/hdf-ambari-mpack-2.1.4.0-5.tar.gz \ --purge \ --verboseambari-server基于docker搭了个mysql集群,稍微记一下,
首先是新建mysql主库容
docker run -d -e MYSQL_ROOT_PASSWORD=admin --name mysql-master -p 3307:3306 mysql:latest-d表示容器运行在后台,-e表示设置环境变量,即MYSQL_ROOT_PASSWORD=admin,设置了mysql的root密码,--name表示容器名,-p表示端口映射,将内部mysql:3306映射为外部的3307,最后的mysql:latest表示镜像名
此外还可以用-v /local_path/my-master.cnf:/etc/mysql/my.cnf来映射配置文件
然后同理启动从库docker run -d -e MYSQL_ROOT_PASSWORD=admin --name mysql-slave -p 3308:3306 mysql:latest
然后进入主库改下配置文件docker-enter mysql-master如果无法进入就用docker ps -a看下容器是否在正常运行,如果status显示
未正常运行,则用docker logs mysql-master看下日志哪里出错了。
进入容器后,我这边使用的镜像的mysqld配置文件是在/etc/mysql下面,这个最新版本的mysql的配置文件包含
三部分,/etc/mysql/my.cnf和/etc/mysql/conf.d/mysql.cnf,还有/etc/mysql/mysql.conf.d/mysqld.cnf
这里需要改的是最后一个,加上
log-bin = mysql-bin
-server_id = 1
-保存后退出容器重启主库容器,然后进入从库更改相同文件,
-log-bin = mysql-bin
-server_id = 2
-同样退出重启容器,然后是配置主从,首先进入主库,用命令mysql -u root -pxxxx进入mysql,然后赋予一个同步
权限GRANT REPLICATION SLAVE ON *.* to 'backup'@'%' identified by '123456';还是同样说明下,ON *.*表示了数
据库全部的权限,如果要指定数据库/表的话可以使用类似testDb/testTable,然后是'backup'@'%'表示给予同步
权限的用户名及其主机ip,%表示不限制ip,当然如果有防火墙的话还是会有限制的,最后的identified by '123456'
表示同步用户的密码,然后就查看下主库的状态信息show master status,如下图:![9G5FE[9%@7%G(B`Q7]E)5@R.png](https://ooo.0o0.ooo/2016/08/10/57aac43029559.png)
把file跟position记下来,然后再开一个terminal,进入从库容器,登陆mysql,然后设置主库
change master to
-master_host='xxx.xxx.xxx.xxx', //如果主从库的容器都在同一个宿主机上,这里的ip是docker容器的ip
-master_user='backup', //就是上面的赋予权限的用户
-master_password='123456',
-master_log_file='mysql-bin.000004', //主库中查看到的file
-master_log_pos=312, //主库中查看到的position
-master_port=3306; //如果是同一宿主机,这里使用真实的3306端口,3308及主库的3307是给外部连接使用的
-通过docker-ip mysql-master可以查看容器的ip
这里有一点是要注意的,也是我踩的坑,就是如果是同一宿主机下两个mysql容器互联,我这里只能通过docker-ip和真实
的3306端口能够连接成功。
本文参考了这位同学的文章
docker最开始是之前在某位大佬的博客看到的,看上去有点神奇,感觉是一种轻量级的虚拟机,但是能做的事情好像差不多,那时候是在Ubuntu系统的vps里起一个Ubuntu的docker,然后在里面装个nginx,配置端口映射就可以访问了,后来也草草写过一篇使用docker搭建mysql集群,但是最近看了下好像是因为装docker的大佬做了一些别名还是什么操作,导致里面用的操作都不具有普遍性,而且主要是把搭的过程写了下,属于囫囵吞枣,没理解docker是干啥的,为啥用docker,就是操作了下,这几天借着搭phabricator的过程,把一些原来不理解,或者原来理解错误的地方重新理一下。
-之前写的 mysql 集群,一主二备,这种架构在很多小型应用里都是这么配置的,而且一般是直接在三台 vps 里启动三个 mysql 实例,但是如果换成 docker 会有什么好处呢,其实就是方便部署,比如其中一台备库挂了,我要加一台,或者说备库的 qps 太高了,需要再加一个,如果要在 vps 上搭建的话,首先要买一台机器,等初始化,然后在上面修改源,更新,装 mysql ,然后配置主从,可能还要处理防火墙等等,如果把这些打包成一个 docker 镜像,并且放在自己的 docker registry,那就直接run 一下就可以了;还有比如在公司要给一个新同学整一套开发测试环境,以 Java 开发为例,要装 git,maven,jdk,配置 maven settings 和各种 rc,整合在一个镜像里的话,就会很方便了;再比如微服务的水平扩展。
-但是为啥 docker 会有这种优势,听起来好像虚拟机也可以干这个事,但是虚拟机动辄上 G,而且需要 VMware,virtual box 等支持,不适合在Linux服务器环境使用,而且占用资源也会非常大。说得这么好,那么 docker 是啥呢
-docker 主要使用 Linux 中已经存在的两种技术的一个整合升级,一个是 namespace,一个是cgroups,相比于虚拟机需要完整虚拟出一个操作系统运行基础,docker 基于宿主机内核,通过 namespace 和 cgroups 分隔进程,理念就是提供一个隔离的最小化运行依赖,这样子相对于虚拟机就有了巨大的便利性,具体的 namespace 和 cgroups 就先不展开讲,可以参考耗子叔的文章
-那么我们先安装下 docker,参考官方的教程,安装,我的系统是 ubuntu 的,就贴了 ubuntu 的链接,用其他系统的可以找到对应的系统文档安装,安装完了的话看看 docker 的信息
-sudo docker info
-
-输出以下信息
然后再来运行个 hello world 呗,
-sudo docker run hello-world
-
-输出了这些
看看这个运行命令是怎么用的,一般都会看到这样子的,sudo docker run -it ubuntu bash, 前面的 docker run 反正就是运行一个容器的意思,-it是啥呢,还有这个什么 ubuntu bash,来看看docker run`的命令帮助信息
-i, --interactive Keep STDIN open even if not attached
-
-就是要有输入,我们运行的时候能输入
--t, --tty Allocate a pseudo-TTY
-
-要有个虚拟终端,
-Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
-
-Run a command in a new container
-
-上面说的-it 就是这里的 options,后面那个 ubuntu 就是 image 辣,image 是啥呢
-Docker 把应用程序及其依赖,打包在 image 文件里面,可以把它理解成为类似于虚拟机的镜像或者运行一个进程的代码,跑起来了的叫docker 容器或者进程,比如我们将要运行的docker run -it ubuntu bash的ubuntu 就是个 ubuntu 容器的镜像,将这个镜像运行起来后,我们可以进入容器像使用 ubuntu 一样使用它,来看下我们的镜像,使用sudo docker image ls就能列出我们宿主机上的 docker 镜像了

一个 ubuntu 镜像才 64MB,非常小巧,然后是后面的bash,我通过交互式启动了一个 ubuntu 容器,然后在这个启动的容器里运行了 bash 命令,这样就可以在容器里玩一下了

只有刚才运行容器的 bash 进程和我刚执行的 ps,这里有个可以注意下的,bash 这个进程的 pid 是 1,其实这里就用到了 linux 中的PID Namespace,容器会隔离出一个 pid 的名字空间,这里面的进程跟外部的 pid 命名独立
-sudo docker ps -a
-
-
这个应该是比较常用的,因为比如是一个微服务容器,有时候就像看下运行状态,日志啥的
-sudo docker exec -it [containerID] bash
-
-
sudo docker logs [containerID]
-
-我在运行容器的终端里胡乱输入点啥,然后通过上面的命令就可以看到啦
-

#!/bin/bash
-while true;do
- echo "1"
-done
-
-直接执行的话就是单核100%的cpu
首先在cgroup下面建个目录
-mkdir -p /sys/fs/cgroup/cpu/sxs_test/
-查看目录下的文件
其中cpuacct开头的表示cpu相关的统计信息,
我们要配置cpu的额度,是在cpu.cfs_quota_us中
echo 2000 > /sys/fs/cgroup/cpu/sxs_test/cpu.cfs_quota_us
-这样表示可以使用2%的cpu,总的配额是在cpu.cfs_period_us中
然后将当前进程输入到cgroup.procs,
-echo $$ > /sys/fs/cgroup/cpu/sxs_test/cgroup.procs
-这样就会自动继承当前进程产生的新进程
再次执行就可以看到cpu被限制了
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
+ docker比一般多一点的初学者介绍四
+ /2022/12/25/docker%E6%AF%94%E4%B8%80%E8%88%AC%E5%A4%9A%E4%B8%80%E7%82%B9%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E4%BB%8B%E7%BB%8D%E5%9B%9B/
+ 这次单独介绍下docker体系里非常重要的cgroup,docker对资源的限制也是基于cgroup构建的,
简单尝试
新建一个shell脚本
+#!/bin/bash
+while true;do
+ echo "1"
+done
-IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
-Someone could be eavesdropping on you right now (man-in-the-middle attack)!
-It is also possible that a host key has just been changed.
-错误信息是这样,有点奇怪也没干啥,网上一搜发现是We updated our RSA SSH host key
简单翻一下就是
--在3月24日协调世界时大约05:00时,出于谨慎,我们更换了用于保护 GitHub.com 的 Git 操作的 RSA SSH 主机密钥。我们这样做是为了保护我们的用户免受任何对手模仿 GitHub 或通过 SSH 窃听他们的 Git 操作的机会。此密钥不授予对 GitHub 基础设施或客户数据的访问权限。此更改仅影响通过使用 RSA 的 SSH 进行的 Git 操作。GitHub.com 和 HTTPS Git 操作的网络流量不受影响。
-
要解决也比较简单就是重置下 host key,
---Host Key是服务器用来证明自己身份的一个永久性的非对称密钥
-
使用
-ssh-keygen -R github.com
-然后在首次建立连接的时候同意下就可以了
+直接执行的话就是单核100%的cpu
首先在cgroup下面建个目录
+mkdir -p /sys/fs/cgroup/cpu/sxs_test/
+查看目录下的文件
其中cpuacct开头的表示cpu相关的统计信息,
我们要配置cpu的额度,是在cpu.cfs_quota_us中
echo 2000 > /sys/fs/cgroup/cpu/sxs_test/cpu.cfs_quota_us
+这样表示可以使用2%的cpu,总的配额是在cpu.cfs_period_us中
然后将当前进程输入到cgroup.procs,
+echo $$ > /sys/fs/cgroup/cpu/sxs_test/cgroup.procs
+这样就会自动继承当前进程产生的新进程
再次执行就可以看到cpu被限制了
4
- / \
- 2 7
- / \ / \
-1 3 6 9
-
-to
- 4
- / \
- 7 2
- / \ / \
-9 6 3 1
-
-Trivia:
This problem was inspired by this original tweet by Max Howell:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
+
+IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
+Someone could be eavesdropping on you right now (man-in-the-middle attack)!
+It is also possible that a host key has just been changed.
+错误信息是这样,有点奇怪也没干啥,网上一搜发现是We updated our RSA SSH host key
简单翻一下就是
--Google: 90% of our engineers use the software you wrote (Homebrew),
+
but you can’t invert a binary tree on a whiteboard so fuck off.在3月24日协调世界时大约05:00时,出于谨慎,我们更换了用于保护 GitHub.com 的 Git 操作的 RSA SSH 主机密钥。我们这样做是为了保护我们的用户免受任何对手模仿 GitHub 或通过 SSH 窃听他们的 Git 操作的机会。此密钥不授予对 GitHub 基础设施或客户数据的访问权限。此更改仅影响通过使用 RSA 的 SSH 进行的 Git 操作。GitHub.com 和 HTTPS Git 操作的网络流量不受影响。
/**
- * Definition for a binary tree node.
- * struct TreeNode {
- * int val;
- * TreeNode *left;
- * TreeNode *right;
- * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
- * };
- */
-class Solution {
-public:
- TreeNode* invertTree(TreeNode* root) {
- if(root == NULL) return root;
- TreeNode* temp;
- temp = invertTree(root->left);
- root->left = invertTree(root->right);
- root->right = temp;
- return root;
- }
-};]]>要解决也比较简单就是重置下 host key,
+++Host Key是服务器用来证明自己身份的一个永久性的非对称密钥
+
使用
+ssh-keygen -R github.com
+然后在首次建立连接的时候同意下就可以了
+]]>select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,#{}替换成了?// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
-public SqlSource parseScriptNode() {
- MixedSqlNode rootSqlNode = parseDynamicTags(context);
- SqlSource sqlSource;
- if (isDynamic) {
- sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
- } else {
- sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
- }
- return sqlSource;
- }
-// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
-protected MixedSqlNode parseDynamicTags(XNode node) {
- List<SqlNode> contents = new ArrayList<>();
- NodeList children = node.getNode().getChildNodes();
- for (int i = 0; i < children.getLength(); i++) {
- XNode child = node.newXNode(children.item(i));
- if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
- String data = child.getStringBody("");
- TextSqlNode textSqlNode = new TextSqlNode(data);
- if (textSqlNode.isDynamic()) {
- contents.add(textSqlNode);
- isDynamic = true;
- } else {
- contents.add(new StaticTextSqlNode(data));
- }
- } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
- String nodeName = child.getNode().getNodeName();
- NodeHandler handler = nodeHandlerMap.get(nodeName);
- if (handler == null) {
- throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
- }
- handler.handleNode(child, contents);
- isDynamic = true;
- }
- }
- return new MixedSqlNode(contents);
- }
-// org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
- public boolean isDynamic() {
- DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
- GenericTokenParser parser = createParser(checker);
- parser.parse(text);
- return checker.isDynamic();
+ mybatis 的缓存是怎么回事
+ /2020/10/03/mybatis-%E7%9A%84%E7%BC%93%E5%AD%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/
+ Java 真的是任何一个中间件,比较常用的那种,都有很多内容值得深挖,比如这个缓存,慢慢有过一些感悟,比如如何提升性能,缓存无疑是一大重要手段,最底层开始 CPU 就有缓存,而且又小又贵,再往上一点内存一般作为硬盘存储在运行时的存储,一般在代码里也会用内存作为一些本地缓存,譬如数据库,像 mysql 这种也是有innodb_buffer_pool来提升查询效率,本质上理解就是用更快的存储作为相对慢存储的缓存,减少查询直接访问较慢的存储,并且这个都是相对的,比起 cpu 的缓存,那内存也是渣,但是与普通机械硬盘相比,那也是两个次元的水平。
+闲扯这么多来说说 mybatis 的缓存,mybatis 一般作为一个轻量级的 orm 使用,相对应的就是比较重量级的 hibernate,不过不在这次讨论范围,上一次是主要讲了 mybatis 在解析 sql 过程中,对于两种占位符的不同替换实现策略,这次主要聊下 mybatis 的缓存,前面其实得了解下前置的东西,比如 sqlsession,先当做我们知道 sqlsession 是个什么玩意,可能或多或少的知道 mybatis 是有两级缓存,
+一级缓存
第一级的缓存是在 BaseExecutor 中的 PerpetualCache,它是个最基本的缓存实现类,使用了 HashMap 实现缓存功能,代码其实没几十行
+public class PerpetualCache implements Cache {
+
+ private final String id;
+
+ private final Map<Object, Object> cache = new HashMap<>();
+
+ public PerpetualCache(String id) {
+ this.id = id;
}
- private GenericTokenParser createParser(TokenHandler handler) {
- return new GenericTokenParser("${", "}", handler);
- }
-可以看到其中一个条件就是是否有${}这种占位符,假如说上面的 sql 换成 ${},那么可以看到它会在这里创建一个 dynamicSqlSource,
-// org.apache.ibatis.scripting.xmltags.DynamicSqlSource
-public class DynamicSqlSource implements SqlSource {
- private final Configuration configuration;
- private final SqlNode rootSqlNode;
+ @Override
+ public String getId() {
+ return id;
+ }
- public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
- this.configuration = configuration;
- this.rootSqlNode = rootSqlNode;
+ @Override
+ public int getSize() {
+ return cache.size();
}
@Override
- public BoundSql getBoundSql(Object parameterObject) {
- DynamicContext context = new DynamicContext(configuration, parameterObject);
- rootSqlNode.apply(context);
- SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
- Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
- SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
- BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
- context.getBindings().forEach(boundSql::setAdditionalParameter);
- return boundSql;
- }
-
-}
-
-这里眼尖的同学可能就可以看出来了,RawSqlSource 在初始化的时候已经经过了 parse,把#{}替换成了?占位符,但是 DynamicSqlSource 并没有
再看这个图,我们发现在这的时候还没有进行替换
然后往里跟
好像是这里了
![]()
这里 rootSqlNode.apply 其实是一个对原来 sql 的解析结果的一个循环调用,不同类型的标签会构成不同的 node,像这里就是一个 textSqlNode
![]()
可以发现到这我们的 sql 已经被替换了,而且是直接作为 string 类型替换的,所以可以明白了这个问题所在,就是注入,不过细心的同学发现其实这里是有个
![]()
理论上还是可以做过滤的,不过好像现在没用起来。
我们前面可以发现对于#{}是在启动扫描 mapper的 xml 文件就替换成了 ?,然后是在什么时候变成实际的值的呢
![]()
发现到这的时候还是没有替换,其实说白了也就是 prepareStatement 那一套,
![]()
在这里进行替换,会拿到 org.apache.ibatis.mapping.ParameterMapping,然后进行替换,因为会带着类型信息,所以不用担心注入咯
-]]>
-
- Java
- Mybatis
- Spring
- Mysql
- Sql注入
- Mybatis
-
-
- Java
- Mysql
- Mybatis
- Sql注入
-
- Given an array of integers, find two numbers such that they add up to a specific target number.
-The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.
- -You may assume that each input would have exactly one solution.
-Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
struct Node
-{
- int num, pos;
-};
-bool cmp(Node a, Node b)
-{
- return a.num < b.num;
-}
-class Solution {
-public:
- vector<int> twoSum(vector<int> &numbers, int target) {
- // Start typing your C/C++ solution below
- // DO NOT write int main() function
- vector<int> result;
- vector<Node> array;
- for (int i = 0; i < numbers.size(); i++)
- {
- Node temp;
- temp.num = numbers[i];
- temp.pos = i;
- array.push_back(temp);
- }
-
- sort(array.begin(), array.end(), cmp);
- for (int i = 0, j = array.size() - 1; i != j;)
- {
- int sum = array[i].num + array[j].num;
- if (sum == target)
- {
- if (array[i].pos < array[j].pos)
- {
- result.push_back(array[i].pos + 1);
- result.push_back(array[j].pos + 1);
- } else
- {
- result.push_back(array[j].pos + 1);
- result.push_back(array[i].pos + 1);
- }
- break;
- } else if (sum < target)
- {
- i++;
- } else if (sum > target)
- {
- j--;
- }
- }
- return result;
- }
-};
-
-sort the array, then test from head and end, until catch the right answer
-]]>A binary watch has 4 LEDs on the top which represent the hours (0-11), and the 6 LEDs on the bottom represent the minutes (0-59).
-Each LED represents a zero or one, with the least significant bit on the right.
-
For example, the above binary watch reads “3:25”.
-Given a non-negative integer n which represents the number of LEDs that are currently on, return all possible times the watch could represent.
-Input: n = 1
-Return: ["1:00", "2:00", "4:00", "8:00", "0:01", "0:02", "0:04", "0:08", "0:16", "0:32"]
-又是参(chao)考(xi)别人的代码,嗯,就是这么不要脸,链接
-class Solution {
-public:
- vector<string> readBinaryWatch(int num) {
- vector<string> res;
- for (int h = 0; h < 12; ++h) {
- for (int m = 0; m < 60; ++m) {
- if (bitset<10>((h << 6) + m).count() == num) {
- res.push_back(to_string(h) + (m < 10 ? ":0" : ":") + to_string(m));
- }
- }
- }
- return res;
- }
-};]]>You are given an integer array nums where the largest integer is unique.
Determine whether the largest element in the array is at least twice as much as every other number in the array. If it is, return the index of the largest element, or return -1 otherwise.
确认在数组中的最大数是否是其余任意数的两倍大及以上,如果是返回索引,如果不是返回-1
--Input: nums = [3,6,1,0]
-
Output: 1
Explanation: 6 is the largest integer.
For every other number in the array x, 6 is at least twice as big as x.
The index of value 6 is 1, so we return 1.
--Input: nums = [1,2,3,4]
-
Output: -1
Explanation: 4 is less than twice the value of 3, so we return -1.
2 <= nums.length <= 500 <= nums[i] <= 100nums is unique.这个题easy是题意也比较简单,找最大值,并且最大值是其他任意值的两倍及以上,其实就是找最大值跟次大值,比较下就好了
-public int dominantIndex(int[] nums) {
- int largest = Integer.MIN_VALUE;
- int second = Integer.MIN_VALUE;
- int largestIndex = -1;
- for (int i = 0; i < nums.length; i++) {
- // 如果有最大的就更新,同时更新最大值和第二大的
- if (nums[i] > largest) {
- second = largest;
- largest = nums[i];
- largestIndex = i;
- } else if (nums[i] > second) {
- // 没有超过最大的,但是比第二大的更大就更新第二大的
- second = nums[i];
- }
- }
-
- // 判断下是否符合题目要求,要是所有值的两倍及以上
- if (largest >= 2 * second) {
- return largestIndex;
- } else {
- return -1;
- }
-}
-第一次错了是把第二大的情况只考虑第一种,也有可能最大值完全没经过替换就变成最大值了
闲扯这么多来说说 mybatis 的缓存,mybatis 一般作为一个轻量级的 orm 使用,相对应的就是比较重量级的 hibernate,不过不在这次讨论范围,上一次是主要讲了 mybatis 在解析 sql 过程中,对于两种占位符的不同替换实现策略,这次主要聊下 mybatis 的缓存,前面其实得了解下前置的东西,比如 sqlsession,先当做我们知道 sqlsession 是个什么玩意,可能或多或少的知道 mybatis 是有两级缓存,
-第一级的缓存是在 BaseExecutor 中的 PerpetualCache,它是个最基本的缓存实现类,使用了 HashMap 实现缓存功能,代码其实没几十行
-public class PerpetualCache implements Cache {
-
- private final String id;
-
- private final Map<Object, Object> cache = new HashMap<>();
-
- public PerpetualCache(String id) {
- this.id = id;
- }
-
- @Override
- public String getId() {
- return id;
- }
-
- @Override
- public int getSize() {
- return cache.size();
- }
-
- @Override
- public void putObject(Object key, Object value) {
- cache.put(key, value);
+ public void putObject(Object key, Object value) {
+ cache.put(key, value);
}
@Override
@@ -6592,342 +6563,447 @@ public:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-
-在构建 SqlSessionFactory 也就是 DefaultSqlSessionFactory 的时候,
-public SqlSessionFactory build(InputStream inputStream) {
- return build(inputStream, null, null);
- }
-public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
- try {
- XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
- return build(parser.parse());
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error building SqlSession.", e);
- } finally {
- ErrorContext.instance().reset();
- try {
- if (inputStream != null) {
- inputStream.close();
- }
- } catch (IOException e) {
- // Intentionally ignore. Prefer previous error.
- }
- }
- }
-前面也说过,就是解析 mybatis-config.xml 成 Configuration
public Configuration parse() {
- if (parsed) {
- throw new BuilderException("Each XMLConfigBuilder can only be used once.");
- }
- parsed = true;
- parseConfiguration(parser.evalNode("/configuration"));
- return configuration;
-}
-private void parseConfiguration(XNode root) {
- try {
- // issue #117 read properties first
- propertiesElement(root.evalNode("properties"));
- Properties settings = settingsAsProperties(root.evalNode("settings"));
- loadCustomVfs(settings);
- loadCustomLogImpl(settings);
- typeAliasesElement(root.evalNode("typeAliases"));
- pluginElement(root.evalNode("plugins"));
- objectFactoryElement(root.evalNode("objectFactory"));
- objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
- reflectorFactoryElement(root.evalNode("reflectorFactory"));
- settingsElement(settings);
- // read it after objectFactory and objectWrapperFactory issue #631
- // -------------> 是在这里解析了DataSource
+ mybatis 的 $ 和 # 是有啥区别
+ /2020/09/06/mybatis-%E7%9A%84-%E5%92%8C-%E6%98%AF%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/
+ 这个问题也是面试中常被问到的,就抽空来了解下这个,跳过一大段前面初始化的逻辑,
对于一条select * from t1 where id = #{id}这样的 sql,在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource,这里它是一条 RawSqlSource,
在这里做了替换,将#{}替换成了?
![]()
前面说的是否 dynamic 就是在这里进行判断
+// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
+public SqlSource parseScriptNode() {
+ MixedSqlNode rootSqlNode = parseDynamicTags(context);
+ SqlSource sqlSource;
+ if (isDynamic) {
+ sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ } else {
+ sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+ }
+ return sqlSource;
+ }
+// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
+protected MixedSqlNode parseDynamicTags(XNode node) {
+ List<SqlNode> contents = new ArrayList<>();
+ NodeList children = node.getNode().getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ XNode child = node.newXNode(children.item(i));
+ if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
+ String data = child.getStringBody("");
+ TextSqlNode textSqlNode = new TextSqlNode(data);
+ if (textSqlNode.isDynamic()) {
+ contents.add(textSqlNode);
+ isDynamic = true;
+ } else {
+ contents.add(new StaticTextSqlNode(data));
+ }
+ } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
+ String nodeName = child.getNode().getNodeName();
+ NodeHandler handler = nodeHandlerMap.get(nodeName);
+ if (handler == null) {
+ throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+ }
+ handler.handleNode(child, contents);
+ isDynamic = true;
+ }
+ }
+ return new MixedSqlNode(contents);
+ }
+// org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
+ public boolean isDynamic() {
+ DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
+ GenericTokenParser parser = createParser(checker);
+ parser.parse(text);
+ return checker.isDynamic();
+ }
+ private GenericTokenParser createParser(TokenHandler handler) {
+ return new GenericTokenParser("${", "}", handler);
+ }
+可以看到其中一个条件就是是否有${}这种占位符,假如说上面的 sql 换成 ${},那么可以看到它会在这里创建一个 dynamicSqlSource,
+// org.apache.ibatis.scripting.xmltags.DynamicSqlSource
+public class DynamicSqlSource implements SqlSource {
+
+ private final Configuration configuration;
+ private final SqlNode rootSqlNode;
+
+ public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
+ this.configuration = configuration;
+ this.rootSqlNode = rootSqlNode;
+ }
+
+ @Override
+ public BoundSql getBoundSql(Object parameterObject) {
+ DynamicContext context = new DynamicContext(configuration, parameterObject);
+ rootSqlNode.apply(context);
+ SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
+ Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
+ SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
+ BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
+ context.getBindings().forEach(boundSql::setAdditionalParameter);
+ return boundSql;
+ }
+
+}
+
+这里眼尖的同学可能就可以看出来了,RawSqlSource 在初始化的时候已经经过了 parse,把#{}替换成了?占位符,但是 DynamicSqlSource 并没有
再看这个图,我们发现在这的时候还没有进行替换
然后往里跟
好像是这里了
![]()
这里 rootSqlNode.apply 其实是一个对原来 sql 的解析结果的一个循环调用,不同类型的标签会构成不同的 node,像这里就是一个 textSqlNode
![]()
可以发现到这我们的 sql 已经被替换了,而且是直接作为 string 类型替换的,所以可以明白了这个问题所在,就是注入,不过细心的同学发现其实这里是有个
![]()
理论上还是可以做过滤的,不过好像现在没用起来。
我们前面可以发现对于#{}是在启动扫描 mapper的 xml 文件就替换成了 ?,然后是在什么时候变成实际的值的呢
![]()
发现到这的时候还是没有替换,其实说白了也就是 prepareStatement 那一套,
![]()
在这里进行替换,会拿到 org.apache.ibatis.mapping.ParameterMapping,然后进行替换,因为会带着类型信息,所以不用担心注入咯
+]]>
+
+ Java
+ Mybatis
+ Spring
+ Mysql
+ Sql注入
+ Mybatis
+
+
+ Java
+ Mysql
+ Mybatis
+ Sql注入
+
+ selectOne跟语句id就能执行sql了,那么第一个问题,就是mapper是怎么被解析的,存在哪里,怎么被拿出来的
+org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
+public SqlSessionFactory build(InputStream inputStream) {
+ return build(inputStream, null, null);
+}
+
+通过读取mybatis-config.xml来构建SqlSessionFactory,
+public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
+ try {
+ // 创建下xml的解析器
+ XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
+ // 进行解析,后再构建
+ return build(parser.parse());
+ } catch (Exception e) {
+ throw ExceptionFactory.wrapException("Error building SqlSession.", e);
+ } finally {
+ ErrorContext.instance().reset();
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ // Intentionally ignore. Prefer previous error.
+ }
+ }
+
+创建XMLConfigBuilder
+public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
+ // --------> 创建 XPathParser
+ this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
+}
+
+public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
+ commonConstructor(validation, variables, entityResolver);
+ this.document = createDocument(new InputSource(inputStream));
+ }
+
+private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
+ super(new Configuration());
+ ErrorContext.instance().resource("SQL Mapper Configuration");
+ this.configuration.setVariables(props);
+ this.parsed = false;
+ this.environment = environment;
+ this.parser = parser;
+}
+
+这里主要是创建了Builder包含了Parser
然后调用parse方法
public Configuration parse() {
+ if (parsed) {
+ throw new BuilderException("Each XMLConfigBuilder can only be used once.");
+ }
+ // 标记下是否已解析,但是这里是否有线程安全问题
+ parsed = true;
+ // --------> 解析配置
+ parseConfiguration(parser.evalNode("/configuration"));
+ return configuration;
+}
+
+实际的解析区分了各类标签
+private void parseConfiguration(XNode root) {
+ try {
+ // issue #117 read properties first
+ // 解析properties,这个不是spring自带的,需要额外配置,并且在config文件里应该放在最前
+ propertiesElement(root.evalNode("properties"));
+ Properties settings = settingsAsProperties(root.evalNode("settings"));
+ loadCustomVfs(settings);
+ loadCustomLogImpl(settings);
+ typeAliasesElement(root.evalNode("typeAliases"));
+ pluginElement(root.evalNode("plugins"));
+ objectFactoryElement(root.evalNode("objectFactory"));
+ objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
+ reflectorFactoryElement(root.evalNode("reflectorFactory"));
+ settingsElement(settings);
+ // read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
+ // ----------> 我们需要关注的是mapper的处理
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
-}
-环境解析了这一块的内容
-<environments default="development">
- <environment id="development">
- <transactionManager type="JDBC"/>
- <dataSource type="POOLED">
- <property name="driver" value="${driver}"/>
- <property name="url" value="${url}"/>
- <property name="username" value="${username}"/>
- <property name="password" value="${password}"/>
- </dataSource>
- </environment>
- </environments>
-解析也是自上而下的,
-private void environmentsElement(XNode context) throws Exception {
- if (context != null) {
- if (environment == null) {
- environment = context.getStringAttribute("default");
- }
- for (XNode child : context.getChildren()) {
- String id = child.getStringAttribute("id");
- if (isSpecifiedEnvironment(id)) {
- TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
- DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
- DataSource dataSource = dsFactory.getDataSource();
- Environment.Builder environmentBuilder = new Environment.Builder(id)
- .transactionFactory(txFactory)
- .dataSource(dataSource);
- configuration.setEnvironment(environmentBuilder.build());
- break;
+}
+
+然后就是调用到mapperElement方法了
+private void mapperElement(XNode parent) throws Exception {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ if ("package".equals(child.getName())) {
+ String mapperPackage = child.getStringAttribute("name");
+ configuration.addMappers(mapperPackage);
+ } else {
+ String resource = child.getStringAttribute("resource");
+ String url = child.getStringAttribute("url");
+ String mapperClass = child.getStringAttribute("class");
+ if (resource != null && url == null && mapperClass == null) {
+ ErrorContext.instance().resource(resource);
+ try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
+ // --------> 我们这没有指定package,所以是走到这
+ mapperParser.parse();
+ }
+ } else if (resource == null && url != null && mapperClass == null) {
+ ErrorContext.instance().resource(url);
+ try(InputStream inputStream = Resources.getUrlAsStream(url)){
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
+ mapperParser.parse();
+ }
+ } else if (resource == null && url == null && mapperClass != null) {
+ Class<?> mapperInterface = Resources.classForName(mapperClass);
+ configuration.addMapper(mapperInterface);
+ } else {
+ throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
+ }
}
}
}
-}
-前面第一步是解析事务管理器元素
-private TransactionFactory transactionManagerElement(XNode context) throws Exception {
- if (context != null) {
- String type = context.getStringAttribute("type");
- Properties props = context.getChildrenAsProperties();
- TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
- factory.setProperties(props);
- return factory;
+}
+
+核心就在这个parse()方法
+public void parse() {
+ if (!configuration.isResourceLoaded(resource)) {
+ // -------> 然后就是走到这里,配置xml的mapper节点的内容
+ configurationElement(parser.evalNode("/mapper"));
+ configuration.addLoadedResource(resource);
+ bindMapperForNamespace();
}
- throw new BuilderException("Environment declaration requires a TransactionFactory.");
-}
-而这里的 resolveClass 其实就使用了上一篇的 typeAliases 系统,这里是使用了 JdbcTransactionFactory 作为事务管理器,
后面的就是 DataSourceFactory 的创建也是 DataSource 的创建
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
- if (context != null) {
- String type = context.getStringAttribute("type");
- Properties props = context.getChildrenAsProperties();
- DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
- factory.setProperties(props);
- return factory;
- }
- throw new BuilderException("Environment declaration requires a DataSourceFactory.");
-}
-因为在config文件中设置了Pooled,所以对应创建的就是 PooledDataSourceFactory
但是这里其实有个比较需要注意的,mybatis 这里的其实是继承了 UnpooledDataSourceFactory
将基础方法都放在了 UnpooledDataSourceFactory 中
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
- public PooledDataSourceFactory() {
- this.dataSource = new PooledDataSource();
+ parsePendingResultMaps();
+ parsePendingCacheRefs();
+ parsePendingStatements();
+}
+
+具体的处理逻辑
+private void configurationElement(XNode context) {
+ try {
+ String namespace = context.getStringAttribute("namespace");
+ if (namespace == null || namespace.isEmpty()) {
+ throw new BuilderException("Mapper's namespace cannot be empty");
+ }
+ builderAssistant.setCurrentNamespace(namespace);
+ cacheRefElement(context.evalNode("cache-ref"));
+ cacheElement(context.evalNode("cache"));
+ parameterMapElement(context.evalNodes("/mapper/parameterMap"));
+ resultMapElements(context.evalNodes("/mapper/resultMap"));
+ sqlElement(context.evalNodes("/mapper/sql"));
+ // -------> 走到这,从上下文构建statement
+ buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
+ } catch (Exception e) {
+ throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
+}
+具体代码在这,从上下文构建statement,只不过区分了下databaseId
private void buildStatementFromContext(List<XNode> list) {
+ if (configuration.getDatabaseId() != null) {
+ buildStatementFromContext(list, configuration.getDatabaseId());
+ }
+ // -----> 判断databaseId
+ buildStatementFromContext(list, null);
}
-这里只保留了在构造方法里创建 DataSource
而这个 PooledDataSource 虽然没有直接继承 UnpooledDataSource,但其实
在构造方法里也是
public PooledDataSource() {
- dataSource = new UnpooledDataSource();
-}
-至于为什么这么做呢应该也是考虑到能比较多的复用代码,因为 Pooled 其实跟 Unpooled 最重要的差别就在于是不是每次都重开连接
使用连接池能够让应用在有大量查询的时候不用反复创建连接,省去了建联的网络等开销,Unpooled就是完成与数据库的连接,并可以获取该连接
主要的代码
@Override
-public Connection getConnection() throws SQLException {
- return doGetConnection(username, password);
-}
-@Override
-public Connection getConnection(String username, String password) throws SQLException {
- return doGetConnection(username, password);
-}
-private Connection doGetConnection(String username, String password) throws SQLException {
- Properties props = new Properties();
- if (driverProperties != null) {
- props.putAll(driverProperties);
- }
- if (username != null) {
- props.setProperty("user", username);
+判断下databaseId
+private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
+ for (XNode context : list) {
+ final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
+ try {
+ // -------> 解析statement节点
+ statementParser.parseStatementNode();
+ } catch (IncompleteElementException e) {
+ configuration.addIncompleteStatement(statementParser);
+ }
}
- if (password != null) {
- props.setProperty("password", password);
+}
+
+接下来就是真正处理的xml语句内容的,各个节点的信息内容
+public void parseStatementNode() {
+ String id = context.getStringAttribute("id");
+ String databaseId = context.getStringAttribute("databaseId");
+
+ if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
+ return;
}
- return doGetConnection(props);
-}
-private Connection doGetConnection(Properties properties) throws SQLException {
- initializeDriver();
- Connection connection = DriverManager.getConnection(url, properties);
- configureConnection(connection);
- return connection;
-}
-而对于Pooled就会处理池化的逻辑
-private PooledConnection popConnection(String username, String password) throws SQLException {
- boolean countedWait = false;
- PooledConnection conn = null;
- long t = System.currentTimeMillis();
- int localBadConnectionCount = 0;
- while (conn == null) {
- lock.lock();
- try {
- if (!state.idleConnections.isEmpty()) {
- // Pool has available connection
- conn = state.idleConnections.remove(0);
- if (log.isDebugEnabled()) {
- log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
- }
- } else {
- // Pool does not have available connection
- if (state.activeConnections.size() < poolMaximumActiveConnections) {
- // Can create new connection
- conn = new PooledConnection(dataSource.getConnection(), this);
- if (log.isDebugEnabled()) {
- log.debug("Created connection " + conn.getRealHashCode() + ".");
- }
- } else {
- // Cannot create new connection
- PooledConnection oldestActiveConnection = state.activeConnections.get(0);
- long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
- if (longestCheckoutTime > poolMaximumCheckoutTime) {
- // Can claim overdue connection
- state.claimedOverdueConnectionCount++;
- state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
- state.accumulatedCheckoutTime += longestCheckoutTime;
- state.activeConnections.remove(oldestActiveConnection);
- if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
- try {
- oldestActiveConnection.getRealConnection().rollback();
- } catch (SQLException e) {
- /*
- Just log a message for debug and continue to execute the following
- statement like nothing happened.
- Wrap the bad connection with a new PooledConnection, this will help
- to not interrupt current executing thread and give current thread a
- chance to join the next competition for another valid/good database
- connection. At the end of this loop, bad {@link @conn} will be set as null.
- */
- log.debug("Bad connection. Could not roll back");
- }
- }
- conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
- conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
- conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
- oldestActiveConnection.invalidate();
- if (log.isDebugEnabled()) {
- log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
- }
- } else {
- // Must wait
- try {
- if (!countedWait) {
- state.hadToWaitCount++;
- countedWait = true;
- }
- if (log.isDebugEnabled()) {
- log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
- }
- long wt = System.currentTimeMillis();
- condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
- state.accumulatedWaitTime += System.currentTimeMillis() - wt;
- } catch (InterruptedException e) {
- // set interrupt flag
- Thread.currentThread().interrupt();
- break;
- }
- }
- }
- }
- if (conn != null) {
- // ping to server and check the connection is valid or not
- if (conn.isValid()) {
- if (!conn.getRealConnection().getAutoCommit()) {
- conn.getRealConnection().rollback();
- }
- conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
- conn.setCheckoutTimestamp(System.currentTimeMillis());
- conn.setLastUsedTimestamp(System.currentTimeMillis());
- state.activeConnections.add(conn);
- state.requestCount++;
- state.accumulatedRequestTime += System.currentTimeMillis() - t;
- } else {
- if (log.isDebugEnabled()) {
- log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
- }
- state.badConnectionCount++;
- localBadConnectionCount++;
- conn = null;
- if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
- if (log.isDebugEnabled()) {
- log.debug("PooledDataSource: Could not get a good connection to the database.");
- }
- throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
- }
- }
- }
- } finally {
- lock.unlock();
- }
+ String nodeName = context.getNode().getNodeName();
+ SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
+ boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
+ boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
+ boolean useCache = context.getBooleanAttribute("useCache", isSelect);
+ boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
- }
+ // Include Fragments before parsing
+ XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
+ includeParser.applyIncludes(context.getNode());
- if (conn == null) {
- if (log.isDebugEnabled()) {
- log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
- }
- throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
- }
+ String parameterType = context.getStringAttribute("parameterType");
+ Class<?> parameterTypeClass = resolveClass(parameterType);
- return conn;
- }
-它的入口不是个get方法,而是pop,从含义来来讲就不一样
org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection()
-@Override
-public Connection getConnection() throws SQLException {
- return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-}
-对于具体怎么获取连接我们可以下一篇具体讲下
-]]>比如这样
-public static void main(String[] args) {
- String selectSql = new SQL() {{
- SELECT("id", "name");
- FROM("student");
- WHERE("id = #{id}");
- }}.toString();
- System.out.println(selectSql);
-}
-打印出来就是
-SELECT id, name
-FROM student
-WHERE (id = #{id})
-应付简单的 sql 查询基本都可以这么解决,如果习惯这种模式,还是不错的,
其实以面向对象的编程模式来说,这样是比较符合面向对象的,先不深入的解析这块的源码,先从使用角度讲一下
String updateSql = new SQL() {{
- UPDATE("student");
- SET("name = #{name}");
- WHERE("id = #{id}");
- }}.toString();
-打印输出就是
-UPDATE student
-SET name = #{name}
-WHERE (id = #{id})
+ String lang = context.getStringAttribute("lang");
+ LanguageDriver langDriver = getLanguageDriver(lang);
-String insertSql = new SQL() {{
- INSERT_INTO("student");
- VALUES("name", "#{name}");
- VALUES("age", "#{age}");
- }}.toString();
- System.out.println(insertSql);
-打印输出
-INSERT INTO student
- (name, age)
-VALUES (#{name}, #{age})
-String deleteSql = new SQL() {{
- DELETE_FROM("student");
- WHERE("id = #{id}");
- }}.toString();
- System.out.println(deleteSql);
-打印输出
-DELETE FROM student
-WHERE (id = #{id})
+ // Parse selectKey after includes and remove them.
+ processSelectKeyNodes(id, parameterTypeClass, langDriver);
+
+ // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
+ KeyGenerator keyGenerator;
+ String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
+ keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
+ if (configuration.hasKeyGenerator(keyStatementId)) {
+ keyGenerator = configuration.getKeyGenerator(keyStatementId);
+ } else {
+ keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
+ configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
+ ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
+ }
+
+ // 语句的主要参数解析
+ SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
+ StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
+ Integer fetchSize = context.getIntAttribute("fetchSize");
+ Integer timeout = context.getIntAttribute("timeout");
+ String parameterMap = context.getStringAttribute("parameterMap");
+ String resultType = context.getStringAttribute("resultType");
+ Class<?> resultTypeClass = resolveClass(resultType);
+ String resultMap = context.getStringAttribute("resultMap");
+ String resultSetType = context.getStringAttribute("resultSetType");
+ ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
+ if (resultSetTypeEnum == null) {
+ resultSetTypeEnum = configuration.getDefaultResultSetType();
+ }
+ String keyProperty = context.getStringAttribute("keyProperty");
+ String keyColumn = context.getStringAttribute("keyColumn");
+ String resultSets = context.getStringAttribute("resultSets");
+
+ // --------> 添加映射的statement
+ builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
+ fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
+ resultSetTypeEnum, flushCache, useCache, resultOrdered,
+ keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
+}
+
+
+添加的逻辑具体可以看下
+public MappedStatement addMappedStatement(
+ String id,
+ SqlSource sqlSource,
+ StatementType statementType,
+ SqlCommandType sqlCommandType,
+ Integer fetchSize,
+ Integer timeout,
+ String parameterMap,
+ Class<?> parameterType,
+ String resultMap,
+ Class<?> resultType,
+ ResultSetType resultSetType,
+ boolean flushCache,
+ boolean useCache,
+ boolean resultOrdered,
+ KeyGenerator keyGenerator,
+ String keyProperty,
+ String keyColumn,
+ String databaseId,
+ LanguageDriver lang,
+ String resultSets) {
+
+ if (unresolvedCacheRef) {
+ throw new IncompleteElementException("Cache-ref not yet resolved");
+ }
+
+ id = applyCurrentNamespace(id, false);
+ boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
+
+ MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
+ .resource(resource)
+ .fetchSize(fetchSize)
+ .timeout(timeout)
+ .statementType(statementType)
+ .keyGenerator(keyGenerator)
+ .keyProperty(keyProperty)
+ .keyColumn(keyColumn)
+ .databaseId(databaseId)
+ .lang(lang)
+ .resultOrdered(resultOrdered)
+ .resultSets(resultSets)
+ .resultMaps(getStatementResultMaps(resultMap, resultType, id))
+ .resultSetType(resultSetType)
+ .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
+ .useCache(valueOrDefault(useCache, isSelect))
+ .cache(currentCache);
+
+ ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
+ if (statementParameterMap != null) {
+ statementBuilder.parameterMap(statementParameterMap);
+ }
+
+ MappedStatement statement = statementBuilder.build();
+ // ------> 正好是这里在configuration中添加了映射好的statement
+ configuration.addMappedStatement(statement);
+ return statement;
+}
+
+而里面就是往map里添加
+public void addMappedStatement(MappedStatement ms) {
+ mappedStatements.put(ms.getId(), ms);
+}
+
+StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
+
+就是调用了 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
public <T> T selectOne(String statement, Object parameter) {
+ // Popular vote was to return null on 0 results and throw exception on too many.
+ List<T> list = this.selectList(statement, parameter);
+ if (list.size() == 1) {
+ return list.get(0);
+ } else if (list.size() > 1) {
+ throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
+ } else {
+ return null;
+ }
+}
+
+调用实际的实现方法
+public <E> List<E> selectList(String statement, Object parameter) {
+ return this.selectList(statement, parameter, RowBounds.DEFAULT);
+}
+
+这里还有一层
+public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
+ return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
+}
+
+
+根本的就是从configuration里获取了mappedStatement
+private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
+ try {
+ // 这里进行了获取
+ MappedStatement ms = configuration.getMappedStatement(statement);
+ return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
+ } catch (Exception e) {
+ throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
+ } finally {
+ ErrorContext.instance().reset();
+ }
+}
]]>selectOne跟语句id就能执行sql了,那么第一个问题,就是mapper是怎么被解析的,存在哪里,怎么被拿出来的
-org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
-public SqlSessionFactory build(InputStream inputStream) {
- return build(inputStream, null, null);
-}
-
-通过读取mybatis-config.xml来构建SqlSessionFactory,
-public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
- try {
- // 创建下xml的解析器
- XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
- // 进行解析,后再构建
- return build(parser.parse());
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error building SqlSession.", e);
- } finally {
- ErrorContext.instance().reset();
+public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
- if (inputStream != null) {
- inputStream.close();
- }
- } catch (IOException e) {
- // Intentionally ignore. Prefer previous error.
+ XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
+ return build(parser.parse());
+ } catch (Exception e) {
+ throw ExceptionFactory.wrapException("Error building SqlSession.", e);
+ } finally {
+ ErrorContext.instance().reset();
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ // Intentionally ignore. Prefer previous error.
+ }
}
- }
-
-创建XMLConfigBuilder
-public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
- // --------> 创建 XPathParser
- this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
-}
-
-public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
- commonConstructor(validation, variables, entityResolver);
- this.document = createDocument(new InputSource(inputStream));
- }
-
-private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
- super(new Configuration());
- ErrorContext.instance().resource("SQL Mapper Configuration");
- this.configuration.setVariables(props);
- this.parsed = false;
- this.environment = environment;
- this.parser = parser;
-}
-
-这里主要是创建了Builder包含了Parser
然后调用parse方法
前面也说过,就是解析 mybatis-config.xml 成 Configuration
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
- // 标记下是否已解析,但是这里是否有线程安全问题
parsed = true;
- // --------> 解析配置
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
-}
-
-实际的解析区分了各类标签
-private void parseConfiguration(XNode root) {
+}
+private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
- // 解析properties,这个不是spring自带的,需要额外配置,并且在config文件里应该放在最前
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
@@ -7359,276 +7277,293 @@ WHERE (id = #{id})(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
+ // -------------> 是在这里解析了DataSource
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
- // ----------> 我们需要关注的是mapper的处理
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
-}
-
-然后就是调用到mapperElement方法了
-private void mapperElement(XNode parent) throws Exception {
- if (parent != null) {
- for (XNode child : parent.getChildren()) {
- if ("package".equals(child.getName())) {
- String mapperPackage = child.getStringAttribute("name");
- configuration.addMappers(mapperPackage);
- } else {
- String resource = child.getStringAttribute("resource");
- String url = child.getStringAttribute("url");
- String mapperClass = child.getStringAttribute("class");
- if (resource != null && url == null && mapperClass == null) {
- ErrorContext.instance().resource(resource);
- try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
- XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
- // --------> 我们这没有指定package,所以是走到这
- mapperParser.parse();
- }
- } else if (resource == null && url != null && mapperClass == null) {
- ErrorContext.instance().resource(url);
- try(InputStream inputStream = Resources.getUrlAsStream(url)){
- XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
- mapperParser.parse();
- }
- } else if (resource == null && url == null && mapperClass != null) {
- Class<?> mapperInterface = Resources.classForName(mapperClass);
- configuration.addMapper(mapperInterface);
- } else {
- throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
- }
+}
+环境解析了这一块的内容
+<environments default="development">
+ <environment id="development">
+ <transactionManager type="JDBC"/>
+ <dataSource type="POOLED">
+ <property name="driver" value="${driver}"/>
+ <property name="url" value="${url}"/>
+ <property name="username" value="${username}"/>
+ <property name="password" value="${password}"/>
+ </dataSource>
+ </environment>
+ </environments>
+解析也是自上而下的,
+private void environmentsElement(XNode context) throws Exception {
+ if (context != null) {
+ if (environment == null) {
+ environment = context.getStringAttribute("default");
+ }
+ for (XNode child : context.getChildren()) {
+ String id = child.getStringAttribute("id");
+ if (isSpecifiedEnvironment(id)) {
+ TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
+ DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
+ DataSource dataSource = dsFactory.getDataSource();
+ Environment.Builder environmentBuilder = new Environment.Builder(id)
+ .transactionFactory(txFactory)
+ .dataSource(dataSource);
+ configuration.setEnvironment(environmentBuilder.build());
+ break;
}
}
}
-}
-
-核心就在这个parse()方法
-public void parse() {
- if (!configuration.isResourceLoaded(resource)) {
- // -------> 然后就是走到这里,配置xml的mapper节点的内容
- configurationElement(parser.evalNode("/mapper"));
- configuration.addLoadedResource(resource);
- bindMapperForNamespace();
+}
+前面第一步是解析事务管理器元素
+private TransactionFactory transactionManagerElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties props = context.getChildrenAsProperties();
+ TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
+ factory.setProperties(props);
+ return factory;
}
-
- parsePendingResultMaps();
- parsePendingCacheRefs();
- parsePendingStatements();
-}
-
-具体的处理逻辑
-private void configurationElement(XNode context) {
- try {
- String namespace = context.getStringAttribute("namespace");
- if (namespace == null || namespace.isEmpty()) {
- throw new BuilderException("Mapper's namespace cannot be empty");
- }
- builderAssistant.setCurrentNamespace(namespace);
- cacheRefElement(context.evalNode("cache-ref"));
- cacheElement(context.evalNode("cache"));
- parameterMapElement(context.evalNodes("/mapper/parameterMap"));
- resultMapElements(context.evalNodes("/mapper/resultMap"));
- sqlElement(context.evalNodes("/mapper/sql"));
- // -------> 走到这,从上下文构建statement
- buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
- } catch (Exception e) {
- throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
+ throw new BuilderException("Environment declaration requires a TransactionFactory.");
+}
+而这里的 resolveClass 其实就使用了上一篇的 typeAliases 系统,这里是使用了 JdbcTransactionFactory 作为事务管理器,
后面的就是 DataSourceFactory 的创建也是 DataSource 的创建
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties props = context.getChildrenAsProperties();
+ DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
+ factory.setProperties(props);
+ return factory;
}
-}
+ throw new BuilderException("Environment declaration requires a DataSourceFactory.");
+}
+因为在config文件中设置了Pooled,所以对应创建的就是 PooledDataSourceFactory
但是这里其实有个比较需要注意的,mybatis 这里的其实是继承了 UnpooledDataSourceFactory
将基础方法都放在了 UnpooledDataSourceFactory 中
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
-具体代码在这,从上下文构建statement,只不过区分了下databaseId
-private void buildStatementFromContext(List<XNode> list) {
- if (configuration.getDatabaseId() != null) {
- buildStatementFromContext(list, configuration.getDatabaseId());
+ public PooledDataSourceFactory() {
+ this.dataSource = new PooledDataSource();
}
- // -----> 判断databaseId
- buildStatementFromContext(list, null);
-}
-判断下databaseId
-private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
- for (XNode context : list) {
- final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
- try {
- // -------> 解析statement节点
- statementParser.parseStatementNode();
- } catch (IncompleteElementException e) {
- configuration.addIncompleteStatement(statementParser);
- }
- }
-}
-
-接下来就是真正处理的xml语句内容的,各个节点的信息内容
-public void parseStatementNode() {
- String id = context.getStringAttribute("id");
- String databaseId = context.getStringAttribute("databaseId");
+}
+这里只保留了在构造方法里创建 DataSource
而这个 PooledDataSource 虽然没有直接继承 UnpooledDataSource,但其实
在构造方法里也是
+public PooledDataSource() {
+ dataSource = new UnpooledDataSource();
+}
+至于为什么这么做呢应该也是考虑到能比较多的复用代码,因为 Pooled 其实跟 Unpooled 最重要的差别就在于是不是每次都重开连接
使用连接池能够让应用在有大量查询的时候不用反复创建连接,省去了建联的网络等开销,Unpooled就是完成与数据库的连接,并可以获取该连接
主要的代码
+@Override
+public Connection getConnection() throws SQLException {
+ return doGetConnection(username, password);
+}
- if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
- return;
+@Override
+public Connection getConnection(String username, String password) throws SQLException {
+ return doGetConnection(username, password);
+}
+private Connection doGetConnection(String username, String password) throws SQLException {
+ Properties props = new Properties();
+ if (driverProperties != null) {
+ props.putAll(driverProperties);
}
-
- String nodeName = context.getNode().getNodeName();
- SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
- boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
- boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
- boolean useCache = context.getBooleanAttribute("useCache", isSelect);
- boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
-
- // Include Fragments before parsing
- XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
- includeParser.applyIncludes(context.getNode());
-
- String parameterType = context.getStringAttribute("parameterType");
- Class<?> parameterTypeClass = resolveClass(parameterType);
-
- String lang = context.getStringAttribute("lang");
- LanguageDriver langDriver = getLanguageDriver(lang);
-
- // Parse selectKey after includes and remove them.
- processSelectKeyNodes(id, parameterTypeClass, langDriver);
-
- // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
- KeyGenerator keyGenerator;
- String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
- keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
- if (configuration.hasKeyGenerator(keyStatementId)) {
- keyGenerator = configuration.getKeyGenerator(keyStatementId);
- } else {
- keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
- configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
- ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
+ if (username != null) {
+ props.setProperty("user", username);
}
-
- // 语句的主要参数解析
- SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
- StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
- Integer fetchSize = context.getIntAttribute("fetchSize");
- Integer timeout = context.getIntAttribute("timeout");
- String parameterMap = context.getStringAttribute("parameterMap");
- String resultType = context.getStringAttribute("resultType");
- Class<?> resultTypeClass = resolveClass(resultType);
- String resultMap = context.getStringAttribute("resultMap");
- String resultSetType = context.getStringAttribute("resultSetType");
- ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
- if (resultSetTypeEnum == null) {
- resultSetTypeEnum = configuration.getDefaultResultSetType();
+ if (password != null) {
+ props.setProperty("password", password);
}
- String keyProperty = context.getStringAttribute("keyProperty");
- String keyColumn = context.getStringAttribute("keyColumn");
- String resultSets = context.getStringAttribute("resultSets");
-
- // --------> 添加映射的statement
- builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
- fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
- resultSetTypeEnum, flushCache, useCache, resultOrdered,
- keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
-}
-
+ return doGetConnection(props);
+}
+private Connection doGetConnection(Properties properties) throws SQLException {
+ initializeDriver();
+ Connection connection = DriverManager.getConnection(url, properties);
+ configureConnection(connection);
+ return connection;
+}
+而对于Pooled就会处理池化的逻辑
+private PooledConnection popConnection(String username, String password) throws SQLException {
+ boolean countedWait = false;
+ PooledConnection conn = null;
+ long t = System.currentTimeMillis();
+ int localBadConnectionCount = 0;
-添加的逻辑具体可以看下
-public MappedStatement addMappedStatement(
- String id,
- SqlSource sqlSource,
- StatementType statementType,
- SqlCommandType sqlCommandType,
- Integer fetchSize,
- Integer timeout,
- String parameterMap,
- Class<?> parameterType,
- String resultMap,
- Class<?> resultType,
- ResultSetType resultSetType,
- boolean flushCache,
- boolean useCache,
- boolean resultOrdered,
- KeyGenerator keyGenerator,
- String keyProperty,
- String keyColumn,
- String databaseId,
- LanguageDriver lang,
- String resultSets) {
+ while (conn == null) {
+ lock.lock();
+ try {
+ if (!state.idleConnections.isEmpty()) {
+ // Pool has available connection
+ conn = state.idleConnections.remove(0);
+ if (log.isDebugEnabled()) {
+ log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
+ }
+ } else {
+ // Pool does not have available connection
+ if (state.activeConnections.size() < poolMaximumActiveConnections) {
+ // Can create new connection
+ conn = new PooledConnection(dataSource.getConnection(), this);
+ if (log.isDebugEnabled()) {
+ log.debug("Created connection " + conn.getRealHashCode() + ".");
+ }
+ } else {
+ // Cannot create new connection
+ PooledConnection oldestActiveConnection = state.activeConnections.get(0);
+ long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
+ if (longestCheckoutTime > poolMaximumCheckoutTime) {
+ // Can claim overdue connection
+ state.claimedOverdueConnectionCount++;
+ state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
+ state.accumulatedCheckoutTime += longestCheckoutTime;
+ state.activeConnections.remove(oldestActiveConnection);
+ if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
+ try {
+ oldestActiveConnection.getRealConnection().rollback();
+ } catch (SQLException e) {
+ /*
+ Just log a message for debug and continue to execute the following
+ statement like nothing happened.
+ Wrap the bad connection with a new PooledConnection, this will help
+ to not interrupt current executing thread and give current thread a
+ chance to join the next competition for another valid/good database
+ connection. At the end of this loop, bad {@link @conn} will be set as null.
+ */
+ log.debug("Bad connection. Could not roll back");
+ }
+ }
+ conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
+ conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
+ conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
+ oldestActiveConnection.invalidate();
+ if (log.isDebugEnabled()) {
+ log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
+ }
+ } else {
+ // Must wait
+ try {
+ if (!countedWait) {
+ state.hadToWaitCount++;
+ countedWait = true;
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
+ }
+ long wt = System.currentTimeMillis();
+ condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
+ state.accumulatedWaitTime += System.currentTimeMillis() - wt;
+ } catch (InterruptedException e) {
+ // set interrupt flag
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+ }
+ if (conn != null) {
+ // ping to server and check the connection is valid or not
+ if (conn.isValid()) {
+ if (!conn.getRealConnection().getAutoCommit()) {
+ conn.getRealConnection().rollback();
+ }
+ conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
+ conn.setCheckoutTimestamp(System.currentTimeMillis());
+ conn.setLastUsedTimestamp(System.currentTimeMillis());
+ state.activeConnections.add(conn);
+ state.requestCount++;
+ state.accumulatedRequestTime += System.currentTimeMillis() - t;
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
+ }
+ state.badConnectionCount++;
+ localBadConnectionCount++;
+ conn = null;
+ if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
+ if (log.isDebugEnabled()) {
+ log.debug("PooledDataSource: Could not get a good connection to the database.");
+ }
+ throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
+ }
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
- if (unresolvedCacheRef) {
- throw new IncompleteElementException("Cache-ref not yet resolved");
- }
+ }
- id = applyCurrentNamespace(id, false);
- boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
-
- MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
- .resource(resource)
- .fetchSize(fetchSize)
- .timeout(timeout)
- .statementType(statementType)
- .keyGenerator(keyGenerator)
- .keyProperty(keyProperty)
- .keyColumn(keyColumn)
- .databaseId(databaseId)
- .lang(lang)
- .resultOrdered(resultOrdered)
- .resultSets(resultSets)
- .resultMaps(getStatementResultMaps(resultMap, resultType, id))
- .resultSetType(resultSetType)
- .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
- .useCache(valueOrDefault(useCache, isSelect))
- .cache(currentCache);
-
- ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
- if (statementParameterMap != null) {
- statementBuilder.parameterMap(statementParameterMap);
- }
-
- MappedStatement statement = statementBuilder.build();
- // ------> 正好是这里在configuration中添加了映射好的statement
- configuration.addMappedStatement(statement);
- return statement;
-}
-
-而里面就是往map里添加
-public void addMappedStatement(MappedStatement ms) {
- mappedStatements.put(ms.getId(), ms);
-}
-
-获取mapper
StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
-
-就是调用了 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
-public <T> T selectOne(String statement, Object parameter) {
- // Popular vote was to return null on 0 results and throw exception on too many.
- List<T> list = this.selectList(statement, parameter);
- if (list.size() == 1) {
- return list.get(0);
- } else if (list.size() > 1) {
- throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
- } else {
- return null;
- }
-}
-
-调用实际的实现方法
-public <E> List<E> selectList(String statement, Object parameter) {
- return this.selectList(statement, parameter, RowBounds.DEFAULT);
-}
-
-这里还有一层
-public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
- return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
-}
+ if (conn == null) {
+ if (log.isDebugEnabled()) {
+ log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+ }
+ throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+ }
+ return conn;
+ }
+它的入口不是个get方法,而是pop,从含义来来讲就不一样org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection()
@Override
+public Connection getConnection() throws SQLException {
+ return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+}
+对于具体怎么获取连接我们可以下一篇具体讲下
+]]>比如这样
+public static void main(String[] args) {
+ String selectSql = new SQL() {{
+ SELECT("id", "name");
+ FROM("student");
+ WHERE("id = #{id}");
+ }}.toString();
+ System.out.println(selectSql);
+}
+打印出来就是
+SELECT id, name
+FROM student
+WHERE (id = #{id})
+应付简单的 sql 查询基本都可以这么解决,如果习惯这种模式,还是不错的,
其实以面向对象的编程模式来说,这样是比较符合面向对象的,先不深入的解析这块的源码,先从使用角度讲一下
String updateSql = new SQL() {{
+ UPDATE("student");
+ SET("name = #{name}");
+ WHERE("id = #{id}");
+ }}.toString();
+打印输出就是
+UPDATE student
+SET name = #{name}
+WHERE (id = #{id})
-根本的就是从configuration里获取了mappedStatement
-private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
- try {
- // 这里进行了获取
- MappedStatement ms = configuration.getMappedStatement(statement);
- return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
- } finally {
- ErrorContext.instance().reset();
- }
-}
+String insertSql = new SQL() {{
+ INSERT_INTO("student");
+ VALUES("name", "#{name}");
+ VALUES("age", "#{age}");
+ }}.toString();
+ System.out.println(insertSql);
+打印输出
+INSERT INTO student
+ (name, age)
+VALUES (#{name}, #{age})
+String deleteSql = new SQL() {{
+ DELETE_FROM("student");
+ WHERE("id = #{id}");
+ }}.toString();
+ System.out.println(deleteSql);
+打印输出
+DELETE FROM student
+WHERE (id = #{id})
]]>首先是日志error_log logs/error.log debug;
需要nginx开启日志的debug才能看到日志
使用 lua_code_cache off即可, 另外注意只有使用 content_by_lua_file 才会生效
http {
- lua_code_cache off;
-}
-
-location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
- default_type 'application/json;charset=utf-8';
- content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
-}
-
-使用lua给nginx请求response头添加内容可以用这个
-ngx.header['response'] = 'header'
+ languageDriverorg/mybatis/mybatis/3.5.11/mybatis-3.5.11-sources.jar!/org/apache/ibatis/session/Configuration.java:215configuration的构造方法里
+languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
+而在org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
中,创建了sqlSource,这里就会根据前面的 LanguageDriver 的实现选择对应的 sqlSource ,
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
-
-后续:
-一开始在本地环境的时候使用content_by_lua_file只关注了头,后来发到测试环境发现请求内容都没代理转发到后端服务上
网上查了下发现content_by_lua_file是将请求的所有内容包括response都用这里面的lua脚本生成了,content这个词就表示是请求内容
后来改成了access_by_lua_file就正常了,只是要去获取请求内容和修改响应头,并不是要完整的接管请求
后来又碰到了一个坑是nginx有个client_body_buffer_size的配置参数,nginx在32位和64位系统里有8K和16K两个默认值,当请求内容大于这两个值的时候,会把请求内容放到临时文件里,这个时候openresty里的ngx.req.get_post_args()就会报“failed to get post args: requesty body in temp file not supported”这个错误,将client_body_buffer_size这个参数配置调大一点就好了
-还有就是lua的异常捕获,网上看一般是用pcall和xpcall来进行保护调用,因为问题主要出在cjson的decode,这里有两个解决方案,一个就是将cjson.decode使用pcall封装,
-local decode = require("cjson").decode
+createSqlSource 就会调用
+@Override
+public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
+ XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
+ return builder.parseScriptNode();
+}
-function json_decode( str )
- local ok, t = pcall(decode, str)
- if not ok then
- return nil
- end
+再往下的逻辑在 parseScriptNode 中,org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
+public SqlSource parseScriptNode() {
+ MixedSqlNode rootSqlNode = parseDynamicTags(context);
+ SqlSource sqlSource;
+ if (isDynamic) {
+ sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ } else {
+ sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+ }
+ return sqlSource;
+}
- return t
-end
- 这个是使用了pcall,称为保护调用,会在内部错误后返回两个参数,第一个是false,第二个是错误信息
还有一种是使用cjson.safe包
local json = require("cjson.safe")
-local str = [[ {"key:"value"} ]]
+首先要解析dynamicTag,调用了org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
+protected MixedSqlNode parseDynamicTags(XNode node) {
+ List<SqlNode> contents = new ArrayList<>();
+ NodeList children = node.getNode().getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ XNode child = node.newXNode(children.item(i));
+ if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
+ String data = child.getStringBody("");
+ TextSqlNode textSqlNode = new TextSqlNode(data);
+ // ---------> 主要是这边的逻辑
+ if (textSqlNode.isDynamic()) {
+ contents.add(textSqlNode);
+ isDynamic = true;
+ } else {
+ contents.add(new StaticTextSqlNode(data));
+ }
+ } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
+ String nodeName = child.getNode().getNodeName();
+ NodeHandler handler = nodeHandlerMap.get(nodeName);
+ if (handler == null) {
+ throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+ }
+ handler.handleNode(child, contents);
+ isDynamic = true;
+ }
+ }
+ return new MixedSqlNode(contents);
+ }
-local t = json.decode(str)
-if t then
- ngx.say(" --> ", type(t))
-end
-cjson.safe包会在解析失败的时候返回nil
-还有一个是redis链接时如果host使用的是域名的话会提示“failed to connect: no resolver defined to resolve “redis.xxxxxx.com””,这里需要使用nginx的resolver指令,
resolver 8.8.8.8 valid=3600s;
还有一点补充下
就是业务在使用redis的时候使用了db的特性,所以在lua访问redis的时候也需要执行db,这里lua的redis库也支持了这个特性,可以使用instance:select(config:get(‘db’))来切换db
发现一个不错的openresty站点
地址
--Perl Compatible Regular Expressions (PCRE) is a regular
-
expression C library inspired by the regular expression
capabilities in the Perl programming language, written
by Philip Hazel, starting in summer 1997.
因为最近工作内容的一部分需要做字符串的识别处理,所以就顺便用上了之前在PHP中用过的正则,在C/C++中本身不包含正则库,这里使用的pcre,对MFC开发,在这里提供了静态链接库,在引入lib跟.h文件后即可使用。
- +判断是否是动态sql,调用了org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
public boolean isDynamic() {
+ DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
+ // ----------> 主要是这里的方法
+ GenericTokenParser parser = createParser(checker);
+ parser.parse(text);
+ return checker.isDynamic();
+}
-然后是一些正则语法,官方的语法文档比较科学严谨,特别是对类似于贪婪匹配等细节的说明,当然一般的使用可以在网上找到很多匹配语法,例如这个。
---pcre_compile
-
原型:
#include <pcre.h>
-pcre *pcre_compile(const char *pattern, int options, const char **errptr, int *erroffset, const unsigned char *tableptr);
-功能:将一个正则表达式编译成一个内部表示,在匹配多个字符串时,可以加速匹配。其同pcre_compile2功能一样只是缺少一个参数errorcodeptr。
参数:pattern 正则表达式options 为0,或者其他参数选项errptr 出错消息erroffset 出错位置tableptr 指向一个字符数组的指针,可以设置为空NULL
--pcre_exec
-
原型:
#include <pcre.h>
-int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, int length, int startoffset, int options, int *ovector, int ovecsize)
-功能:使用编译好的模式进行匹配,采用与Perl相似的算法,返回匹配串的偏移位置。
参数:code 编译好的模式extra 指向一个pcre_extra结构体,可以为NULLsubject 需要匹配的字符串length 匹配的字符串长度(Byte)startoffset 匹配的开始位置options 选项位ovector 指向一个结果的整型数组ovecsize 数组大小。
这里是两个最常用的函数的简单说明,pcre的静态库提供了一系列的函数以供使用,可以参考这个博客说明,另外对于以上函数的具体参数详细说明可以参考官网此处
-void COcxDemoDlg::pcre_exec_all(const pcre * re, PCRE_SPTR src, vector<pair<int, int>> &vc)
-{
- int rc;
- int ovector[30];
- int i = 0;
- pair<int, int> pr;
- rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
- for (; rc > 0;)
- {
- i = ovector[1];
- pr.first = ovector[2];
- pr.second = ovector[3];
- vc.push_back(pr);
- rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
- }
-}
-vector中是全文匹配后的索引对,只是简单地用下。
-]]><?php
-interface int1{
- const INTER1 = 111;
- function inter1();
-}
-interface int2{
- const INTER1 = 222;
- function inter2();
-}
-abstract class abst1{
- public function abstr1(){
- echo 1111;
- }
- abstract function abstra1(){
- echo 'ahahahha';
- }
-}
-abstract class abst2{
- public function abstr2(){
- echo 1111;
- }
- abstract function abstra2();
-}
-class normal1 extends abst1{
- protected function abstr2(){
- echo 222;
- }
-}
+创建parser的时候可以看到这个parser是干了啥,其实就是找有没有${ , }
private GenericTokenParser createParser(TokenHandler handler) {
+ return new GenericTokenParser("${", "}", handler);
+}
-PHP Fatal error: Abstract function abst1::abstra1() cannot contain body in new.php on line 17
+如果是的话,就在上面把 isDynamic 设置为true 如果是true 的话就创建 DynamicSqlSource
+sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
-Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17
-]]>org.apache.ibatis.builder.xml.XMLConfigBuilder 创建了parser解析器,那么解析的结果是什么public Configuration parse() {
- if (parsed) {
- throw new BuilderException("Each XMLConfigBuilder can only be used once.");
- }
- parsed = true;
- parseConfiguration(parser.evalNode("/configuration"));
- return configuration;
-}
+如果不是的话就创建RawSqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+```java
-返回的是 org.apache.ibatis.session.Configuration , 而这个 Configuration 也是 mybatis 中特别重要的配置核心类,贴一下里面的成员变量,
-public class Configuration {
+但是这不是一个真实可用的 `sqlSource` ,
+实际创建的时候会走到这
+```java
+public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
+ this(configuration, getSql(configuration, rootSqlNode), parameterType);
+ }
- protected Environment environment;
+ public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
+ SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
+ Class<?> clazz = parameterType == null ? Object.class : parameterType;
+ sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
+ }
- protected boolean safeRowBoundsEnabled;
- protected boolean safeResultHandlerEnabled = true;
- protected boolean mapUnderscoreToCamelCase;
- protected boolean aggressiveLazyLoading;
- protected boolean multipleResultSetsEnabled = true;
- protected boolean useGeneratedKeys;
- protected boolean useColumnLabel = true;
- protected boolean cacheEnabled = true;
- protected boolean callSettersOnNulls;
- protected boolean useActualParamName = true;
- protected boolean returnInstanceForEmptyRow;
- protected boolean shrinkWhitespacesInSql;
- protected boolean nullableOnForEach;
- protected boolean argNameBasedConstructorAutoMapping;
+具体的sqlSource是通过org.apache.ibatis.builder.SqlSourceBuilder#parse 创建的
具体的代码逻辑是
+public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
+ ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
+ GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
+ String sql;
+ if (configuration.isShrinkWhitespacesInSql()) {
+ sql = parser.parse(removeExtraWhitespaces(originalSql));
+ } else {
+ sql = parser.parse(originalSql);
+ }
+ return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
+}
- protected String logPrefix;
- protected Class<? extends Log> logImpl;
- protected Class<? extends VFS> vfsImpl;
- protected Class<?> defaultSqlProviderType;
- protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
- protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
- protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
- protected Integer defaultStatementTimeout;
- protected Integer defaultFetchSize;
- protected ResultSetType defaultResultSetType;
- protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
- protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
- protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
-
- protected Properties variables = new Properties();
- protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
- protected ObjectFactory objectFactory = new DefaultObjectFactory();
- protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
-
- protected boolean lazyLoadingEnabled = false;
- protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
+这里创建的其实是StaticSqlSource ,多带一句前面的parser是将原来这样select * from student where id = #{id} 的 sql 解析成了select * from student where id = ? 然后创建了StaticSqlSource
+public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
+ this.sql = sql;
+ this.parameterMappings = parameterMappings;
+ this.configuration = configuration;
+}
- protected String databaseId;
- /**
- * Configuration factory class.
- * Used to create Configuration for loading deserialized unread properties.
- *
- * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
- */
- protected Class<?> configurationFactory;
+为什么前面要讲这么多好像没什么关系的代码呢,其实在最开始我们执行sql的代码中
+@Override
+ public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
+ BoundSql boundSql = ms.getBoundSql(parameterObject);
+ CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
+ return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+ }
- protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
- protected final InterceptorChain interceptorChain = new InterceptorChain();
- protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
- protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
- protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
+这里获取了BoundSql,而BoundSql是怎么来的呢,首先调用了org.apache.ibatis.mapping.MappedStatement#getBoundSql
+public BoundSql getBoundSql(Object parameterObject) {
+ BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
+ List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
+ if (parameterMappings == null || parameterMappings.isEmpty()) {
+ boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
+ }
- protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
- .conflictMessageProducer((savedValue, targetValue) ->
- ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
- protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
- protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
- protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
- protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
+ // check for nested result maps in parameter mappings (issue #30)
+ for (ParameterMapping pm : boundSql.getParameterMappings()) {
+ String rmId = pm.getResultMapId();
+ if (rmId != null) {
+ ResultMap rm = configuration.getResultMap(rmId);
+ if (rm != null) {
+ hasNestedResultMaps |= rm.hasNestedResultMaps();
+ }
+ }
+ }
- protected final Set<String> loadedResources = new HashSet<>();
- protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
+ return boundSql;
+ }
- protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
- protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
- protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
- protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
+而我们从上面的解析中可以看到这里的sqlSource是一层RawSqlSource , 它的getBoundSql又是调用内部的sqlSource的方法
@Override
+public BoundSql getBoundSql(Object parameterObject) {
+ return sqlSource.getBoundSql(parameterObject);
+}
-这么多成员变量,先不一一解释作用,但是其中的几个参数我们应该是已经知道了的,第一个就是 mappedStatements ,上一篇我们知道被解析的mapper就是放在这里,后面的 resultMaps ,parameterMaps 也比较常用的就是我们参数和结果的映射map,这里跟我之前有一篇解释为啥我们一些变量的使用会比较特殊,比如list,可以参考这篇,keyGenerators是在我们需要定义主键生成器的时候使用。
然后第二点是我们创建的 org.apache.ibatis.session.SqlSessionFactory 是哪个,
public SqlSessionFactory build(Configuration config) {
- return new DefaultSqlSessionFactory(config);
-}
+内部的sqlSource 就是StaticSqlSource ,
@Override
+public BoundSql getBoundSql(Object parameterObject) {
+ return new BoundSql(configuration, sql, parameterMappings, parameterObject);
+}
-是这个 DefaultSqlSessionFactory ,这是其中一个 SqlSessionFactory 的实现
接下来我们看看 openSession 里干了啥
public SqlSession openSession() {
- return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
-}
+这个BoundSql的内容也比较简单
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
+ this.sql = sql;
+ this.parameterMappings = parameterMappings;
+ this.parameterObject = parameterObject;
+ this.additionalParameters = new HashMap<>();
+ this.metaParameters = configuration.newMetaObject(additionalParameters);
+}
-这边有几个参数,第一个是默认的执行器类型,往上找找上面贴着的 Configuration 的成员变量里可以看到默认是protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
因为没有指明特殊的执行逻辑,所以默认我们也就用简单类型的,第二个参数是是事务级别,第三个是是否自动提交
-private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
- Transaction tx = null;
+而上次在这边org.apache.ibatis.executor.SimpleExecutor#doQuery 的时候落了个东西,就是StatementHandler的逻辑
+@Override
+public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+ Statement stmt = null;
try {
- final Environment environment = configuration.getEnvironment();
- final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
- tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
- // --------> 先关注这里
- final Executor executor = configuration.newExecutor(tx, execType);
- return new DefaultSqlSession(configuration, executor, autoCommit);
- } catch (Exception e) {
- closeTransaction(tx); // may have fetched a connection so lets call close()
- throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
+ Configuration configuration = ms.getConfiguration();
+ StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
+ stmt = prepareStatement(handler, ms.getStatementLog());
+ return handler.query(stmt, resultHandler);
} finally {
- ErrorContext.instance().reset();
+ closeStatement(stmt);
}
-}
+}
-具体是调用了 Configuration 的这个方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
- executorType = executorType == null ? defaultExecutorType : executorType;
- Executor executor;
- if (ExecutorType.BATCH == executorType) {
- executor = new BatchExecutor(this, transaction);
- } else if (ExecutorType.REUSE == executorType) {
- executor = new ReuseExecutor(this, transaction);
- } else {
- // ---------> 会走到这个分支
- executor = new SimpleExecutor(this, transaction);
- }
- if (cacheEnabled) {
- executor = new CachingExecutor(executor);
+它是通过statementType来区分应该使用哪个statementHandler,我们这使用的就是PreparedStatementHandler
+public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+
+ switch (ms.getStatementType()) {
+ case STATEMENT:
+ delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
+ break;
+ case PREPARED:
+ delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
+ break;
+ case CALLABLE:
+ delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
+ break;
+ default:
+ throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
- executor = (Executor) interceptorChain.pluginAll(executor);
- return executor;
-}
-上面传入的 executorType 是 Configuration 的默认类型,也就是 simple 类型,并且 cacheEnabled 在 Configuration 默认为 true,所以会包装成CachingExecutor ,然后后面就是插件了,这块我们先不展开
然后我们的openSession返回的就是创建了DefaultSqlSession
-public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
- this.configuration = configuration;
- this.executor = executor;
- this.dirty = false;
- this.autoCommit = autoCommit;
- }
+}
-然后就是调用 selectOne, 因为前面已经把这部分代码说过了,就直接跳转过来org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
+所以上次有个细节可以补充,这边的doQuery里面的handler.query 应该是调用了PreparedStatementHandler 的query方法
+@Override
+public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+ Statement stmt = null;
try {
- MappedStatement ms = configuration.getMappedStatement(statement);
- return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
- } catch (Exception e) {
- throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
+ Configuration configuration = ms.getConfiguration();
+ StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
+ stmt = prepareStatement(handler, ms.getStatementLog());
+ return handler.query(stmt, resultHandler);
} finally {
- ErrorContext.instance().reset();
+ closeStatement(stmt);
}
-}
+}
-因为前面说了 executor 包装了 CachingExecutor ,所以会先调用
因为上面prepareStatement中getConnection拿到connection是com.mysql.cj.jdbc.ConnectionImpl#ConnectionImpl(com.mysql.cj.conf.HostInfo)
@Override
-public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameterObject);
- CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
- return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
+ PreparedStatement ps = (PreparedStatement) statement;
+ ps.execute();
+ return resultSetHandler.handleResultSets(ps);
}
-然后是调用的真实的query方法
-@Override
-public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
- throws SQLException {
- Cache cache = ms.getCache();
- if (cache != null) {
- flushCacheIfRequired(ms);
- if (ms.isUseCache() && resultHandler == null) {
- ensureNoOutParams(ms, boundSql);
- @SuppressWarnings("unchecked")
- List<E> list = (List<E>) tcm.getObject(cache, key);
- if (list == null) {
- list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- tcm.putObject(cache, key, list); // issue #578 and #116
- }
- return list;
- }
- }
- return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
-}
+那又为什么是这个呢,可以在网上找,我们在mybatis-config.xml里配置的
+<transactionManager type="JDBC"/>
-这里是第一次查询,没有缓存就先到最后一行,继续是调用到 org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
@Override
- public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
- ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
- if (closed) {
- throw new ExecutorException("Executor was closed.");
+因此在parseConfiguration中配置environment时
+private void parseConfiguration(XNode root) {
+ try {
+ // issue #117 read properties first
+ propertiesElement(root.evalNode("properties"));
+ Properties settings = settingsAsProperties(root.evalNode("settings"));
+ loadCustomVfs(settings);
+ loadCustomLogImpl(settings);
+ typeAliasesElement(root.evalNode("typeAliases"));
+ pluginElement(root.evalNode("plugins"));
+ objectFactoryElement(root.evalNode("objectFactory"));
+ objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
+ reflectorFactoryElement(root.evalNode("reflectorFactory"));
+ settingsElement(settings);
+ // read it after objectFactory and objectWrapperFactory issue #631
+ // ----------> 就是这里
+ environmentsElement(root.evalNode("environments"));
+ databaseIdProviderElement(root.evalNode("databaseIdProvider"));
+ typeHandlerElement(root.evalNode("typeHandlers"));
+ mapperElement(root.evalNode("mappers"));
+ } catch (Exception e) {
+ throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
- if (queryStack == 0 && ms.isFlushCacheRequired()) {
- clearLocalCache();
+ }
+
+调用的这个方法通过获取xml中的transactionManager 配置的类型,也就是JDBC
+private void environmentsElement(XNode context) throws Exception {
+ if (context != null) {
+ if (environment == null) {
+ environment = context.getStringAttribute("default");
}
- List<E> list;
- try {
- queryStack++;
- list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
- if (list != null) {
- handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
- } else {
- // ----------->会走到这里
- list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
- }
- } finally {
- queryStack--;
- }
- if (queryStack == 0) {
- for (DeferredLoad deferredLoad : deferredLoads) {
- deferredLoad.load();
- }
- // issue #601
- deferredLoads.clear();
- if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
- // issue #482
- clearLocalCache();
+ for (XNode child : context.getChildren()) {
+ String id = child.getStringAttribute("id");
+ if (isSpecifiedEnvironment(id)) {
+ // -------> 找到这里
+ TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
+ DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
+ DataSource dataSource = dsFactory.getDataSource();
+ Environment.Builder environmentBuilder = new Environment.Builder(id)
+ .transactionFactory(txFactory)
+ .dataSource(dataSource);
+ configuration.setEnvironment(environmentBuilder.build());
+ break;
}
}
- return list;
- }
-
-然后是
-private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
- List<E> list;
- localCache.putObject(key, EXECUTION_PLACEHOLDER);
- try {
- list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
- } finally {
- localCache.removeObject(key);
}
- localCache.putObject(key, list);
- if (ms.getStatementType() == StatementType.CALLABLE) {
- localOutputParameterCache.putObject(key, parameter);
+}
+
+是通过以下方法获取的,
+// 方法全限定名 org.apache.ibatis.builder.xml.XMLConfigBuilder#transactionManagerElement
+private TransactionFactory transactionManagerElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties props = context.getChildrenAsProperties();
+ TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
+ factory.setProperties(props);
+ return factory;
+ }
+ throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
- return list;
-}
-然后就是 simpleExecutor 的执行过程
-@Override
-public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
- Statement stmt = null;
- try {
- Configuration configuration = ms.getConfiguration();
- StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
- stmt = prepareStatement(handler, ms.getStatementLog());
- return handler.query(stmt, resultHandler);
- } finally {
- closeStatement(stmt);
+// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveClass
+protected <T> Class<? extends T> resolveClass(String alias) {
+ if (alias == null) {
+ return null;
+ }
+ try {
+ return resolveAlias(alias);
+ } catch (Exception e) {
+ throw new BuilderException("Error resolving class. Cause: " + e, e);
+ }
}
-}
-接下去其实就是跟jdbc交互了
-@Override
-public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
- PreparedStatement ps = (PreparedStatement) statement;
- ps.execute();
- return resultSetHandler.handleResultSets(ps);
-}
+// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveAlias
+ protected <T> Class<? extends T> resolveAlias(String alias) {
+ return typeAliasRegistry.resolveAlias(alias);
+ }
+// 方法全限定名 org.apache.ibatis.type.TypeAliasRegistry#resolveAlias
+ public <T> Class<T> resolveAlias(String string) {
+ try {
+ if (string == null) {
+ return null;
+ }
+ // issue #748
+ String key = string.toLowerCase(Locale.ENGLISH);
+ Class<T> value;
+ if (typeAliases.containsKey(key)) {
+ value = (Class<T>) typeAliases.get(key);
+ } else {
+ value = (Class<T>) Resources.classForName(string);
+ }
+ return value;
+ } catch (ClassNotFoundException e) {
+ throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
+ }
+ }
+而通过JDBC获取得是啥的,就是在Configuration的构造方法里写了的JdbcTransactionFactory
+public Configuration() {
+ typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
-com.mysql.cj.jdbc.ClientPreparedStatement#execute
public boolean execute() throws SQLException {
- try {
- synchronized(this.checkClosed().getConnectionMutex()) {
- JdbcConnection locallyScopedConn = this.connection;
- if (!this.doPingInstead && !this.checkReadOnlySafeStatement()) {
- throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.exceptionInterceptor);
- } else {
- ResultSetInternalMethods rs = null;
- this.lastQueryIsOnDupKeyUpdate = false;
- if (this.retrieveGeneratedKeys) {
- this.lastQueryIsOnDupKeyUpdate = this.containsOnDuplicateKeyUpdate();
- }
+所以我们在这
+private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
+ Transaction tx = null;
+ try {
+ final Environment environment = configuration.getEnvironment();
+ final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
- this.batchedGeneratedKeys = null;
- this.resetCancelledState();
- this.implicitlyCloseAllOpenResults();
- this.clearWarnings();
- if (this.doPingInstead) {
- this.doPingInstead();
- return true;
- } else {
- this.setupStreamingTimeout(locallyScopedConn);
- Message sendPacket = ((PreparedQuery)this.query).fillSendPacket(((PreparedQuery)this.query).getQueryBindings());
- String oldDb = null;
- if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
- oldDb = locallyScopedConn.getDatabase();
- locallyScopedConn.setDatabase(this.getCurrentDatabase());
- }
+获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后
+tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
+```java
- CachedResultSetMetaData cachedMetadata = null;
- boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
- if (cacheResultSetMetadata) {
- cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql());
- }
+创建的transaction就是JdbcTransaction
+```java
+ @Override
+ public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
+ return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose);
+ }
- locallyScopedConn.setSessionMaxRows(this.getQueryInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
- rs = this.executeInternal(this.maxRows, sendPacket, this.createStreamingResultSet(), this.getQueryInfo().getFirstStmtChar() == 'S', cachedMetadata, false);
- if (cachedMetadata != null) {
- locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), cachedMetadata, rs);
- } else if (rs.hasRows() && cacheResultSetMetadata) {
- locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), (CachedResultSetMetaData)null, rs);
- }
+然后我们再会上去看代码getConnection ,
+protected Connection getConnection(Log statementLog) throws SQLException {
+ // -------> 这里的transaction就是JdbcTransaction
+ Connection connection = transaction.getConnection();
+ if (statementLog.isDebugEnabled()) {
+ return ConnectionLogger.newInstance(connection, statementLog, queryStack);
+ } else {
+ return connection;
+ }
+}
- if (this.retrieveGeneratedKeys) {
- rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
- }
+即调用了
+ @Override
+ public Connection getConnection() throws SQLException {
+ if (connection == null) {
+ openConnection();
+ }
+ return connection;
+ }
- if (oldDb != null) {
- locallyScopedConn.setDatabase(oldDb);
- }
+ protected void openConnection() throws SQLException {
+ if (log.isDebugEnabled()) {
+ log.debug("Opening JDBC Connection");
+ }
+ connection = dataSource.getConnection();
+ if (level != null) {
+ connection.setTransactionIsolation(level.getLevel());
+ }
+ setDesiredAutoCommit(autoCommit);
+ }
+ @Override
+ public Connection getConnection() throws SQLException {
+ return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+ }
- if (rs != null) {
- this.lastInsertId = rs.getUpdateID();
- this.results = rs;
- }
+private PooledConnection popConnection(String username, String password) throws SQLException {
+ boolean countedWait = false;
+ PooledConnection conn = null;
+ long t = System.currentTimeMillis();
+ int localBadConnectionCount = 0;
- return rs != null && rs.hasRows();
- }
- }
+ while (conn == null) {
+ lock.lock();
+ try {
+ if (!state.idleConnections.isEmpty()) {
+ // Pool has available connection
+ conn = state.idleConnections.remove(0);
+ if (log.isDebugEnabled()) {
+ log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
+ }
+ } else {
+ // Pool does not have available connection
+ if (state.activeConnections.size() < poolMaximumActiveConnections) {
+ // Can create new connection
+ // ------------> 走到这里会创建PooledConnection,但是里面会先调用dataSource.getConnection()
+ conn = new PooledConnection(dataSource.getConnection(), this);
+ if (log.isDebugEnabled()) {
+ log.debug("Created connection " + conn.getRealHashCode() + ".");
}
- } catch (CJException var11) {
- throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
- }
- }
-
-]]>接触了一下rabbitmq,原来在选型的时候是在rabbitmq跟kafka之间做选择,网上搜了一下之后发现kafka的优势在于吞吐量,而rabbitmq相对注重可靠性,因为应用在im上,需要保证消息不能丢失所以就暂时选定rabbitmq,
Message Queue的需求由来已久,80年代最早在金融交易中,高盛等公司采用Teknekron公司的产品,当时的Message queuing软件叫做:the information bus(TIB)。 TIB被电信和通讯公司采用,路透社收购了Teknekron公司。之后,IBM开发了MQSeries,微软开发了Microsoft Message Queue(MSMQ)。这些商业MQ供应商的问题是厂商锁定,价格高昂。2001年,Java Message queuing试图解决锁定和交互性的问题,但对应用来说反而更加麻烦了。
RabbitMQ采用Erlang语言开发。Erlang语言由Ericson设计,专门为开发concurrent和distribution系统的一种语言,在电信领域使用广泛。OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件/库/工具,如mnesia/SASL,极大方便了Erlang应用的开发。OTP就类似于Python语言中众多的module,用户借助这些module可以很方便的开发应用。
于是2004年,摩根大通和iMatrix开始着手Advanced Message Queuing Protocol (AMQP)开放标准的开发。2006年,AMQP规范发布。2007年,Rabbit技术公司基于AMQP标准开发的RabbitMQ 1.0 发布。所有主要的编程语言均有与代理接口通讯的客户端库。
这里介绍下其中的一些概念,connection表示和队列服务器的连接,一般情况下是tcp连接, channel表示通道,可以在一个连接上建立多个通道,这里主要是节省了tcp连接握手的成本,exchange可以理解成一个路由器,将消息推送给对应的队列queue,其实是像一个订阅的模式。
-rabbitmqctl stop这个是关闭rabbitmq,在搭建集群时候先关闭服务,然后使用rabbitmq-server -detached静默启动,这时候使用rabbitmqctl cluster_status查看集群状态,因为还没将节点加入集群,所以只能看到类似
Cluster status of node rabbit@rabbit1 ...
-[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]},
- {running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}]
-...done.
-然后就可以把当前节点加入集群,
-rabbit2$ rabbitmqctl stop_app #这个stop_app与stop的区别是前者停的是rabbitmq应用,保留erlang节点,
- #后者是停止了rabbitmq和erlang节点
-Stopping node rabbit@rabbit2 ...done.
-rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1 #这里可以用--ram指定将当前节点作为内存节点加入集群
-Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done.
-rabbit2$ rabbitmqctl start_app
-Starting node rabbit@rabbit2 ...done.
-其他可以参考官方文档
-这里碰到过一个坑,对于使用exchange来做消息路由的,会有一个情况,就是在routing_key没被订阅的时候,会将该条找不到路由对应的queue的消息丢掉What happens if we break our contract and send a message with one or four words, like "orange" or "quick.orange.male.rabbit"? Well, these messages won't match any bindings and will be lost.对应链接,而当使用空的exchange时,会保留消息,当出现消费者的时候就可以将收到之前生产者所推送的消息对应链接,这里就是用了空的exchange。
集群搭建的时候有个erlang vm生成的random cookie,这个是用来做集群之间认证的,相同的cookie才能连接,但是如果通过vim打开复制后在其他几点新建文件写入会多一个换行,导致集群建立是报错,所以这里最好使用scp等传输命令直接传输cookie文件,同时要注意下cookie的文件权限。
另外在集群搭建的时候如果更改过hostname,那么要把rabbitmq的数据库删除,否则启动后会马上挂掉
languageDriverorg/mybatis/mybatis/3.5.11/mybatis-3.5.11-sources.jar!/org/apache/ibatis/session/Configuration.java:215configuration的构造方法里
-languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
-
-而在org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
中,创建了sqlSource,这里就会根据前面的 LanguageDriver 的实现选择对应的 sqlSource ,
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
-
-createSqlSource 就会调用
@Override
-public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
- XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
- return builder.parseScriptNode();
-}
-
-再往下的逻辑在 parseScriptNode 中,org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
public SqlSource parseScriptNode() {
- MixedSqlNode rootSqlNode = parseDynamicTags(context);
- SqlSource sqlSource;
- if (isDynamic) {
- sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
- } else {
- sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
- }
- return sqlSource;
-}
-
-首先要解析dynamicTag,调用了org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
protected MixedSqlNode parseDynamicTags(XNode node) {
- List<SqlNode> contents = new ArrayList<>();
- NodeList children = node.getNode().getChildNodes();
- for (int i = 0; i < children.getLength(); i++) {
- XNode child = node.newXNode(children.item(i));
- if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
- String data = child.getStringBody("");
- TextSqlNode textSqlNode = new TextSqlNode(data);
- // ---------> 主要是这边的逻辑
- if (textSqlNode.isDynamic()) {
- contents.add(textSqlNode);
- isDynamic = true;
- } else {
- contents.add(new StaticTextSqlNode(data));
+ } else {
+ // Cannot create new connection
+ PooledConnection oldestActiveConnection = state.activeConnections.get(0);
+ long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
+ if (longestCheckoutTime > poolMaximumCheckoutTime) {
+ // Can claim overdue connection
+ state.claimedOverdueConnectionCount++;
+ state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
+ state.accumulatedCheckoutTime += longestCheckoutTime;
+ state.activeConnections.remove(oldestActiveConnection);
+ if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
+ try {
+ oldestActiveConnection.getRealConnection().rollback();
+ } catch (SQLException e) {
+ /*
+ Just log a message for debug and continue to execute the following
+ statement like nothing happened.
+ Wrap the bad connection with a new PooledConnection, this will help
+ to not interrupt current executing thread and give current thread a
+ chance to join the next competition for another valid/good database
+ connection. At the end of this loop, bad {@link @conn} will be set as null.
+ */
+ log.debug("Bad connection. Could not roll back");
+ }
+ }
+ conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
+ conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
+ conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
+ oldestActiveConnection.invalidate();
+ if (log.isDebugEnabled()) {
+ log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
+ }
+ } else {
+ // Must wait
+ try {
+ if (!countedWait) {
+ state.hadToWaitCount++;
+ countedWait = true;
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
+ }
+ long wt = System.currentTimeMillis();
+ condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
+ state.accumulatedWaitTime += System.currentTimeMillis() - wt;
+ } catch (InterruptedException e) {
+ // set interrupt flag
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
}
- } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
- String nodeName = child.getNode().getNodeName();
- NodeHandler handler = nodeHandlerMap.get(nodeName);
- if (handler == null) {
- throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
+ if (conn != null) {
+ // ping to server and check the connection is valid or not
+ if (conn.isValid()) {
+ if (!conn.getRealConnection().getAutoCommit()) {
+ conn.getRealConnection().rollback();
+ }
+ conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
+ conn.setCheckoutTimestamp(System.currentTimeMillis());
+ conn.setLastUsedTimestamp(System.currentTimeMillis());
+ state.activeConnections.add(conn);
+ state.requestCount++;
+ state.accumulatedRequestTime += System.currentTimeMillis() - t;
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
+ }
+ state.badConnectionCount++;
+ localBadConnectionCount++;
+ conn = null;
+ if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
+ if (log.isDebugEnabled()) {
+ log.debug("PooledDataSource: Could not get a good connection to the database.");
+ }
+ throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
+ }
+ }
}
- handler.handleNode(child, contents);
- isDynamic = true;
+ } finally {
+ lock.unlock();
}
- }
- return new MixedSqlNode(contents);
- }
-判断是否是动态sql,调用了org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
public boolean isDynamic() {
- DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
- // ----------> 主要是这里的方法
- GenericTokenParser parser = createParser(checker);
- parser.parse(text);
- return checker.isDynamic();
-}
+ }
-创建parser的时候可以看到这个parser是干了啥,其实就是找有没有${ , }
private GenericTokenParser createParser(TokenHandler handler) {
- return new GenericTokenParser("${", "}", handler);
-}
+ if (conn == null) {
+ if (log.isDebugEnabled()) {
+ log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+ }
+ throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+ }
-如果是的话,就在上面把 isDynamic 设置为true 如果是true 的话就创建 DynamicSqlSource
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
+ return conn;
+ }
-如果不是的话就创建RawSqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
+其实就是调用的
+// org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
+ @Override
+ public Connection getConnection() throws SQLException {
+ return doGetConnection(username, password);
+ }
```java
-但是这不是一个真实可用的 `sqlSource` ,
-实际创建的时候会走到这
+然后就是
```java
-public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
- this(configuration, getSql(configuration, rootSqlNode), parameterType);
- }
-
- public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
- SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
- Class<?> clazz = parameterType == null ? Object.class : parameterType;
- sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
- }
+private Connection doGetConnection(String username, String password) throws SQLException {
+ Properties props = new Properties();
+ if (driverProperties != null) {
+ props.putAll(driverProperties);
+ }
+ if (username != null) {
+ props.setProperty("user", username);
+ }
+ if (password != null) {
+ props.setProperty("password", password);
+ }
+ return doGetConnection(props);
+ }
-具体的sqlSource是通过org.apache.ibatis.builder.SqlSourceBuilder#parse 创建的
具体的代码逻辑是
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
- ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
- GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
- String sql;
- if (configuration.isShrinkWhitespacesInSql()) {
- sql = parser.parse(removeExtraWhitespaces(originalSql));
- } else {
- sql = parser.parse(originalSql);
+继续这个逻辑
+ private Connection doGetConnection(Properties properties) throws SQLException {
+ initializeDriver();
+ Connection connection = DriverManager.getConnection(url, properties);
+ configureConnection(connection);
+ return connection;
}
- return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
-}
-
-这里创建的其实是StaticSqlSource ,多带一句前面的parser是将原来这样select * from student where id = #{id} 的 sql 解析成了select * from student where id = ? 然后创建了StaticSqlSource
-public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
- this.sql = sql;
- this.parameterMappings = parameterMappings;
- this.configuration = configuration;
-}
-
-为什么前面要讲这么多好像没什么关系的代码呢,其实在最开始我们执行sql的代码中
-@Override
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameterObject);
- CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
- return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
+ @CallerSensitive
+ public static Connection getConnection(String url,
+ java.util.Properties info) throws SQLException {
-这里获取了BoundSql,而BoundSql是怎么来的呢,首先调用了org.apache.ibatis.mapping.MappedStatement#getBoundSql
-public BoundSql getBoundSql(Object parameterObject) {
- BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
- List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
- if (parameterMappings == null || parameterMappings.isEmpty()) {
- boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
+ return (getConnection(url, info, Reflection.getCallerClass()));
}
+private static Connection getConnection(
+ String url, java.util.Properties info, Class<?> caller) throws SQLException {
+ /*
+ * When callerCl is null, we should check the application's
+ * (which is invoking this class indirectly)
+ * classloader, so that the JDBC driver class outside rt.jar
+ * can be loaded from here.
+ */
+ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
+ synchronized(DriverManager.class) {
+ // synchronize loading of the correct classloader.
+ if (callerCL == null) {
+ callerCL = Thread.currentThread().getContextClassLoader();
+ }
+ }
- // check for nested result maps in parameter mappings (issue #30)
- for (ParameterMapping pm : boundSql.getParameterMappings()) {
- String rmId = pm.getResultMapId();
- if (rmId != null) {
- ResultMap rm = configuration.getResultMap(rmId);
- if (rm != null) {
- hasNestedResultMaps |= rm.hasNestedResultMaps();
+ if(url == null) {
+ throw new SQLException("The url cannot be null", "08001");
}
- }
- }
- return boundSql;
- }
+ println("DriverManager.getConnection(\"" + url + "\")");
-而我们从上面的解析中可以看到这里的sqlSource是一层RawSqlSource , 它的getBoundSql又是调用内部的sqlSource的方法
-@Override
-public BoundSql getBoundSql(Object parameterObject) {
- return sqlSource.getBoundSql(parameterObject);
-}
+ // Walk through the loaded registeredDrivers attempting to make a connection.
+ // Remember the first exception that gets raised so we can reraise it.
+ SQLException reason = null;
-内部的sqlSource 就是StaticSqlSource ,
-@Override
-public BoundSql getBoundSql(Object parameterObject) {
- return new BoundSql(configuration, sql, parameterMappings, parameterObject);
-}
+ for(DriverInfo aDriver : registeredDrivers) {
+ // If the caller does not have permission to load the driver then
+ // skip it.
+ if(isDriverAllowed(aDriver.driver, callerCL)) {
+ try {
+ // ----------> driver[className=com.mysql.cj.jdbc.Driver@64030b91]
+ println(" trying " + aDriver.driver.getClass().getName());
+ Connection con = aDriver.driver.connect(url, info);
+ if (con != null) {
+ // Success!
+ println("getConnection returning " + aDriver.driver.getClass().getName());
+ return (con);
+ }
+ } catch (SQLException ex) {
+ if (reason == null) {
+ reason = ex;
+ }
+ }
-这个BoundSql的内容也比较简单
-public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
- this.sql = sql;
- this.parameterMappings = parameterMappings;
- this.parameterObject = parameterObject;
- this.additionalParameters = new HashMap<>();
- this.metaParameters = configuration.newMetaObject(additionalParameters);
-}
+ } else {
+ println(" skipping: " + aDriver.getClass().getName());
+ }
-而上次在这边org.apache.ibatis.executor.SimpleExecutor#doQuery 的时候落了个东西,就是StatementHandler的逻辑
-@Override
-public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
- Statement stmt = null;
- try {
- Configuration configuration = ms.getConfiguration();
- StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
- stmt = prepareStatement(handler, ms.getStatementLog());
- return handler.query(stmt, resultHandler);
- } finally {
- closeStatement(stmt);
- }
-}
+ }
-它是通过statementType来区分应该使用哪个statementHandler,我们这使用的就是PreparedStatementHandler
-public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+ // if we got here nobody could connect.
+ if (reason != null) {
+ println("getConnection failed: " + reason);
+ throw reason;
+ }
- switch (ms.getStatementType()) {
- case STATEMENT:
- delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
- break;
- case PREPARED:
- delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
- break;
- case CALLABLE:
- delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
- break;
- default:
- throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
- }
+ println("getConnection: no suitable driver found for "+ url);
+ throw new SQLException("No suitable driver found for "+ url, "08001");
+ }
-}
-所以上次有个细节可以补充,这边的doQuery里面的handler.query 应该是调用了PreparedStatementHandler 的query方法
-@Override
-public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
- Statement stmt = null;
- try {
- Configuration configuration = ms.getConfiguration();
- StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
- stmt = prepareStatement(handler, ms.getStatementLog());
- return handler.query(stmt, resultHandler);
- } finally {
- closeStatement(stmt);
- }
-}
+上面的driver就是driver[className=com.mysql.cj.jdbc.Driver@64030b91]
+// com.mysql.cj.jdbc.NonRegisteringDriver#connect
+public Connection connect(String url, Properties info) throws SQLException {
+ try {
+ try {
+ if (!ConnectionUrl.acceptsUrl(url)) {
+ return null;
+ } else {
+ ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
+ switch (conStr.getType()) {
+ case SINGLE_CONNECTION:
+ return ConnectionImpl.getInstance(conStr.getMainHost());
+ case FAILOVER_CONNECTION:
+ case FAILOVER_DNS_SRV_CONNECTION:
+ return FailoverConnectionProxy.createProxyInstance(conStr);
+ case LOADBALANCE_CONNECTION:
+ case LOADBALANCE_DNS_SRV_CONNECTION:
+ return LoadBalancedConnectionProxy.createProxyInstance(conStr);
+ case REPLICATION_CONNECTION:
+ case REPLICATION_DNS_SRV_CONNECTION:
+ return ReplicationConnectionProxy.createProxyInstance(conStr);
+ default:
+ return null;
+ }
+ }
+ } catch (UnsupportedConnectionStringException var5) {
+ return null;
+ } catch (CJException var6) {
+ throw (UnableToConnectException)ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("NonRegisteringDriver.17", new Object[]{var6.toString()}), var6);
+ }
+ } catch (CJException var7) {
+ throw SQLExceptionsMapping.translateException(var7);
+ }
+ }
+这是个 SINGLE_CONNECTION ,所以调用的就是 return ConnectionImpl.getInstance(conStr.getMainHost());
然后在这里设置了代理类
public PooledConnection(Connection connection, PooledDataSource dataSource) {
+ this.hashCode = connection.hashCode();
+ this.realConnection = connection;
+ this.dataSource = dataSource;
+ this.createdTimestamp = System.currentTimeMillis();
+ this.lastUsedTimestamp = System.currentTimeMillis();
+ this.valid = true;
+ this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
+ }
-因为上面prepareStatement中getConnection拿到connection是com.mysql.cj.jdbc.ConnectionImpl#ConnectionImpl(com.mysql.cj.conf.HostInfo)
+结合这个
@Override
-public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
- PreparedStatement ps = (PreparedStatement) statement;
- ps.execute();
- return resultSetHandler.handleResultSets(ps);
-}
+public Connection getConnection() throws SQLException {
+ return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
+}
-那又为什么是这个呢,可以在网上找,我们在mybatis-config.xml里配置的
-<transactionManager type="JDBC"/>
+所以最终的connection就是com.mysql.cj.jdbc.ConnectionImpl@358ab600
+]]>org.apache.ibatis.builder.xml.XMLConfigBuilder 创建了parser解析器,那么解析的结果是什么public Configuration parse() {
+ if (parsed) {
+ throw new BuilderException("Each XMLConfigBuilder can only be used once.");
+ }
+ parsed = true;
+ parseConfiguration(parser.evalNode("/configuration"));
+ return configuration;
+}
-因此在parseConfiguration中配置environment时
-private void parseConfiguration(XNode root) {
- try {
- // issue #117 read properties first
- propertiesElement(root.evalNode("properties"));
- Properties settings = settingsAsProperties(root.evalNode("settings"));
- loadCustomVfs(settings);
- loadCustomLogImpl(settings);
- typeAliasesElement(root.evalNode("typeAliases"));
- pluginElement(root.evalNode("plugins"));
- objectFactoryElement(root.evalNode("objectFactory"));
- objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
- reflectorFactoryElement(root.evalNode("reflectorFactory"));
- settingsElement(settings);
- // read it after objectFactory and objectWrapperFactory issue #631
- // ----------> 就是这里
- environmentsElement(root.evalNode("environments"));
- databaseIdProviderElement(root.evalNode("databaseIdProvider"));
- typeHandlerElement(root.evalNode("typeHandlers"));
- mapperElement(root.evalNode("mappers"));
- } catch (Exception e) {
- throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
- }
- }
+返回的是 org.apache.ibatis.session.Configuration , 而这个 Configuration 也是 mybatis 中特别重要的配置核心类,贴一下里面的成员变量,
public class Configuration {
-调用的这个方法通过获取xml中的transactionManager 配置的类型,也就是JDBC
-private void environmentsElement(XNode context) throws Exception {
- if (context != null) {
- if (environment == null) {
- environment = context.getStringAttribute("default");
- }
- for (XNode child : context.getChildren()) {
- String id = child.getStringAttribute("id");
- if (isSpecifiedEnvironment(id)) {
- // -------> 找到这里
- TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
- DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
- DataSource dataSource = dsFactory.getDataSource();
- Environment.Builder environmentBuilder = new Environment.Builder(id)
- .transactionFactory(txFactory)
- .dataSource(dataSource);
- configuration.setEnvironment(environmentBuilder.build());
- break;
- }
- }
- }
-}
+ protected Environment environment;
-是通过以下方法获取的,
-// 方法全限定名 org.apache.ibatis.builder.xml.XMLConfigBuilder#transactionManagerElement
-private TransactionFactory transactionManagerElement(XNode context) throws Exception {
- if (context != null) {
- String type = context.getStringAttribute("type");
- Properties props = context.getChildrenAsProperties();
- TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
- factory.setProperties(props);
- return factory;
- }
- throw new BuilderException("Environment declaration requires a TransactionFactory.");
- }
+ protected boolean safeRowBoundsEnabled;
+ protected boolean safeResultHandlerEnabled = true;
+ protected boolean mapUnderscoreToCamelCase;
+ protected boolean aggressiveLazyLoading;
+ protected boolean multipleResultSetsEnabled = true;
+ protected boolean useGeneratedKeys;
+ protected boolean useColumnLabel = true;
+ protected boolean cacheEnabled = true;
+ protected boolean callSettersOnNulls;
+ protected boolean useActualParamName = true;
+ protected boolean returnInstanceForEmptyRow;
+ protected boolean shrinkWhitespacesInSql;
+ protected boolean nullableOnForEach;
+ protected boolean argNameBasedConstructorAutoMapping;
-// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveClass
-protected <T> Class<? extends T> resolveClass(String alias) {
- if (alias == null) {
- return null;
- }
- try {
- return resolveAlias(alias);
- } catch (Exception e) {
- throw new BuilderException("Error resolving class. Cause: " + e, e);
- }
- }
+ protected String logPrefix;
+ protected Class<? extends Log> logImpl;
+ protected Class<? extends VFS> vfsImpl;
+ protected Class<?> defaultSqlProviderType;
+ protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
+ protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
+ protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
+ protected Integer defaultStatementTimeout;
+ protected Integer defaultFetchSize;
+ protected ResultSetType defaultResultSetType;
+ protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
+ protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
+ protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
-// 方法全限定名 org.apache.ibatis.builder.BaseBuilder#resolveAlias
- protected <T> Class<? extends T> resolveAlias(String alias) {
- return typeAliasRegistry.resolveAlias(alias);
- }
-// 方法全限定名 org.apache.ibatis.type.TypeAliasRegistry#resolveAlias
- public <T> Class<T> resolveAlias(String string) {
- try {
- if (string == null) {
- return null;
- }
- // issue #748
- String key = string.toLowerCase(Locale.ENGLISH);
- Class<T> value;
- if (typeAliases.containsKey(key)) {
- value = (Class<T>) typeAliases.get(key);
- } else {
- value = (Class<T>) Resources.classForName(string);
- }
- return value;
- } catch (ClassNotFoundException e) {
- throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
- }
- }
-而通过JDBC获取得是啥的,就是在Configuration的构造方法里写了的JdbcTransactionFactory
-public Configuration() {
- typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
+ protected Properties variables = new Properties();
+ protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
+ protected ObjectFactory objectFactory = new DefaultObjectFactory();
+ protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
-所以我们在这
+ protected boolean lazyLoadingEnabled = false;
+ protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
+
+ protected String databaseId;
+ /**
+ * Configuration factory class.
+ * Used to create Configuration for loading deserialized unread properties.
+ *
+ * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
+ */
+ protected Class<?> configurationFactory;
+
+ protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
+ protected final InterceptorChain interceptorChain = new InterceptorChain();
+ protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
+ protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
+ protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
+
+ protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
+ .conflictMessageProducer((savedValue, targetValue) ->
+ ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
+ protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
+ protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
+ protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
+ protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
+
+ protected final Set<String> loadedResources = new HashSet<>();
+ protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
+
+ protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
+ protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
+ protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
+ protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
+
+这么多成员变量,先不一一解释作用,但是其中的几个参数我们应该是已经知道了的,第一个就是 mappedStatements ,上一篇我们知道被解析的mapper就是放在这里,后面的 resultMaps ,parameterMaps 也比较常用的就是我们参数和结果的映射map,这里跟我之前有一篇解释为啥我们一些变量的使用会比较特殊,比如list,可以参考这篇,keyGenerators是在我们需要定义主键生成器的时候使用。
然后第二点是我们创建的 org.apache.ibatis.session.SqlSessionFactory 是哪个,
public SqlSessionFactory build(Configuration config) {
+ return new DefaultSqlSessionFactory(config);
+}
+
+是这个 DefaultSqlSessionFactory ,这是其中一个 SqlSessionFactory 的实现
接下来我们看看 openSession 里干了啥
public SqlSession openSession() {
+ return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
+}
+
+这边有几个参数,第一个是默认的执行器类型,往上找找上面贴着的 Configuration 的成员变量里可以看到默认是protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
因为没有指明特殊的执行逻辑,所以默认我们也就用简单类型的,第二个参数是是事务级别,第三个是是否自动提交
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
- final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
+ final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
+ tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
+ // --------> 先关注这里
+ final Executor executor = configuration.newExecutor(tx, execType);
+ return new DefaultSqlSession(configuration, executor, autoCommit);
+ } catch (Exception e) {
+ closeTransaction(tx); // may have fetched a connection so lets call close()
+ throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
+ } finally {
+ ErrorContext.instance().reset();
+ }
+}
-获得到的TransactionFactory 就是 JdbcTransactionFactory ,而后
-tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
-```java
+具体是调用了 Configuration 的这个方法
+public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
+ executorType = executorType == null ? defaultExecutorType : executorType;
+ Executor executor;
+ if (ExecutorType.BATCH == executorType) {
+ executor = new BatchExecutor(this, transaction);
+ } else if (ExecutorType.REUSE == executorType) {
+ executor = new ReuseExecutor(this, transaction);
+ } else {
+ // ---------> 会走到这个分支
+ executor = new SimpleExecutor(this, transaction);
+ }
+ if (cacheEnabled) {
+ executor = new CachingExecutor(executor);
+ }
+ executor = (Executor) interceptorChain.pluginAll(executor);
+ return executor;
+}
-创建的transaction就是JdbcTransaction
-```java
- @Override
- public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
- return new JdbcTransaction(ds, level, autoCommit, skipSetAutoCommitOnClose);
- }
+上面传入的 executorType 是 Configuration 的默认类型,也就是 simple 类型,并且 cacheEnabled 在 Configuration 默认为 true,所以会包装成CachingExecutor ,然后后面就是插件了,这块我们先不展开
然后我们的openSession返回的就是创建了DefaultSqlSession
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
+ this.configuration = configuration;
+ this.executor = executor;
+ this.dirty = false;
+ this.autoCommit = autoCommit;
+ }
-然后我们再会上去看代码getConnection ,
-protected Connection getConnection(Log statementLog) throws SQLException {
- // -------> 这里的transaction就是JdbcTransaction
- Connection connection = transaction.getConnection();
- if (statementLog.isDebugEnabled()) {
- return ConnectionLogger.newInstance(connection, statementLog, queryStack);
- } else {
- return connection;
+然后就是调用 selectOne, 因为前面已经把这部分代码说过了,就直接跳转过来
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
+private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
+ try {
+ MappedStatement ms = configuration.getMappedStatement(statement);
+ return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
+ } catch (Exception e) {
+ throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
+ } finally {
+ ErrorContext.instance().reset();
}
-}
+}
-即调用了
- @Override
- public Connection getConnection() throws SQLException {
- if (connection == null) {
- openConnection();
+因为前面说了 executor 包装了 CachingExecutor ,所以会先调用
+@Override
+public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
+ BoundSql boundSql = ms.getBoundSql(parameterObject);
+ CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
+ return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+}
+
+然后是调用的真实的query方法
+@Override
+public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
+ throws SQLException {
+ Cache cache = ms.getCache();
+ if (cache != null) {
+ flushCacheIfRequired(ms);
+ if (ms.isUseCache() && resultHandler == null) {
+ ensureNoOutParams(ms, boundSql);
+ @SuppressWarnings("unchecked")
+ List<E> list = (List<E>) tcm.getObject(cache, key);
+ if (list == null) {
+ list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+ tcm.putObject(cache, key, list); // issue #578 and #116
+ }
+ return list;
}
- return connection;
}
+ return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
+}
- protected void openConnection() throws SQLException {
- if (log.isDebugEnabled()) {
- log.debug("Opening JDBC Connection");
+这里是第一次查询,没有缓存就先到最后一行,继续是调用到 org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
+@Override
+ public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
+ ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
+ if (closed) {
+ throw new ExecutorException("Executor was closed.");
}
- connection = dataSource.getConnection();
- if (level != null) {
- connection.setTransactionIsolation(level.getLevel());
+ if (queryStack == 0 && ms.isFlushCacheRequired()) {
+ clearLocalCache();
}
- setDesiredAutoCommit(autoCommit);
- }
- @Override
- public Connection getConnection() throws SQLException {
- return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
- }
-
-private PooledConnection popConnection(String username, String password) throws SQLException {
- boolean countedWait = false;
- PooledConnection conn = null;
- long t = System.currentTimeMillis();
- int localBadConnectionCount = 0;
-
- while (conn == null) {
- lock.lock();
- try {
- if (!state.idleConnections.isEmpty()) {
- // Pool has available connection
- conn = state.idleConnections.remove(0);
- if (log.isDebugEnabled()) {
- log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
- }
- } else {
- // Pool does not have available connection
- if (state.activeConnections.size() < poolMaximumActiveConnections) {
- // Can create new connection
- // ------------> 走到这里会创建PooledConnection,但是里面会先调用dataSource.getConnection()
- conn = new PooledConnection(dataSource.getConnection(), this);
- if (log.isDebugEnabled()) {
- log.debug("Created connection " + conn.getRealHashCode() + ".");
- }
- } else {
- // Cannot create new connection
- PooledConnection oldestActiveConnection = state.activeConnections.get(0);
- long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
- if (longestCheckoutTime > poolMaximumCheckoutTime) {
- // Can claim overdue connection
- state.claimedOverdueConnectionCount++;
- state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
- state.accumulatedCheckoutTime += longestCheckoutTime;
- state.activeConnections.remove(oldestActiveConnection);
- if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
- try {
- oldestActiveConnection.getRealConnection().rollback();
- } catch (SQLException e) {
- /*
- Just log a message for debug and continue to execute the following
- statement like nothing happened.
- Wrap the bad connection with a new PooledConnection, this will help
- to not interrupt current executing thread and give current thread a
- chance to join the next competition for another valid/good database
- connection. At the end of this loop, bad {@link @conn} will be set as null.
- */
- log.debug("Bad connection. Could not roll back");
- }
- }
- conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
- conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
- conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
- oldestActiveConnection.invalidate();
- if (log.isDebugEnabled()) {
- log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
- }
- } else {
- // Must wait
- try {
- if (!countedWait) {
- state.hadToWaitCount++;
- countedWait = true;
- }
- if (log.isDebugEnabled()) {
- log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
- }
- long wt = System.currentTimeMillis();
- condition.await(poolTimeToWait, TimeUnit.MILLISECONDS);
- state.accumulatedWaitTime += System.currentTimeMillis() - wt;
- } catch (InterruptedException e) {
- // set interrupt flag
- Thread.currentThread().interrupt();
- break;
- }
- }
- }
- }
- if (conn != null) {
- // ping to server and check the connection is valid or not
- if (conn.isValid()) {
- if (!conn.getRealConnection().getAutoCommit()) {
- conn.getRealConnection().rollback();
- }
- conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
- conn.setCheckoutTimestamp(System.currentTimeMillis());
- conn.setLastUsedTimestamp(System.currentTimeMillis());
- state.activeConnections.add(conn);
- state.requestCount++;
- state.accumulatedRequestTime += System.currentTimeMillis() - t;
- } else {
- if (log.isDebugEnabled()) {
- log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
- }
- state.badConnectionCount++;
- localBadConnectionCount++;
- conn = null;
- if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
- if (log.isDebugEnabled()) {
- log.debug("PooledDataSource: Could not get a good connection to the database.");
- }
- throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
- }
- }
- }
- } finally {
- lock.unlock();
+ List<E> list;
+ try {
+ queryStack++;
+ list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
+ if (list != null) {
+ handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
+ } else {
+ // ----------->会走到这里
+ list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
-
+ } finally {
+ queryStack--;
}
-
- if (conn == null) {
- if (log.isDebugEnabled()) {
- log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
+ if (queryStack == 0) {
+ for (DeferredLoad deferredLoad : deferredLoads) {
+ deferredLoad.load();
+ }
+ // issue #601
+ deferredLoads.clear();
+ if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
+ // issue #482
+ clearLocalCache();
}
- throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
+ return list;
+ }
- return conn;
- }
-
-其实就是调用的
-// org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
- @Override
- public Connection getConnection() throws SQLException {
- return doGetConnection(username, password);
+然后是
+private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
+ List<E> list;
+ localCache.putObject(key, EXECUTION_PLACEHOLDER);
+ try {
+ list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
+ } finally {
+ localCache.removeObject(key);
}
-```java
-
-然后就是
-```java
-private Connection doGetConnection(String username, String password) throws SQLException {
- Properties props = new Properties();
- if (driverProperties != null) {
- props.putAll(driverProperties);
- }
- if (username != null) {
- props.setProperty("user", username);
- }
- if (password != null) {
- props.setProperty("password", password);
- }
- return doGetConnection(props);
- }
+ localCache.putObject(key, list);
+ if (ms.getStatementType() == StatementType.CALLABLE) {
+ localOutputParameterCache.putObject(key, parameter);
+ }
+ return list;
+}
-继续这个逻辑
- private Connection doGetConnection(Properties properties) throws SQLException {
- initializeDriver();
- Connection connection = DriverManager.getConnection(url, properties);
- configureConnection(connection);
- return connection;
+然后就是 simpleExecutor 的执行过程
+@Override
+public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+ Statement stmt = null;
+ try {
+ Configuration configuration = ms.getConfiguration();
+ StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
+ stmt = prepareStatement(handler, ms.getStatementLog());
+ return handler.query(stmt, resultHandler);
+ } finally {
+ closeStatement(stmt);
}
- @CallerSensitive
- public static Connection getConnection(String url,
- java.util.Properties info) throws SQLException {
+}
- return (getConnection(url, info, Reflection.getCallerClass()));
- }
-private static Connection getConnection(
- String url, java.util.Properties info, Class<?> caller) throws SQLException {
- /*
- * When callerCl is null, we should check the application's
- * (which is invoking this class indirectly)
- * classloader, so that the JDBC driver class outside rt.jar
- * can be loaded from here.
- */
- ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
- synchronized(DriverManager.class) {
- // synchronize loading of the correct classloader.
- if (callerCL == null) {
- callerCL = Thread.currentThread().getContextClassLoader();
- }
- }
+接下去其实就是跟jdbc交互了
+@Override
+public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
+ PreparedStatement ps = (PreparedStatement) statement;
+ ps.execute();
+ return resultSetHandler.handleResultSets(ps);
+}
- if(url == null) {
- throw new SQLException("The url cannot be null", "08001");
- }
+com.mysql.cj.jdbc.ClientPreparedStatement#execute
+public boolean execute() throws SQLException {
+ try {
+ synchronized(this.checkClosed().getConnectionMutex()) {
+ JdbcConnection locallyScopedConn = this.connection;
+ if (!this.doPingInstead && !this.checkReadOnlySafeStatement()) {
+ throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.exceptionInterceptor);
+ } else {
+ ResultSetInternalMethods rs = null;
+ this.lastQueryIsOnDupKeyUpdate = false;
+ if (this.retrieveGeneratedKeys) {
+ this.lastQueryIsOnDupKeyUpdate = this.containsOnDuplicateKeyUpdate();
+ }
- println("DriverManager.getConnection(\"" + url + "\")");
+ this.batchedGeneratedKeys = null;
+ this.resetCancelledState();
+ this.implicitlyCloseAllOpenResults();
+ this.clearWarnings();
+ if (this.doPingInstead) {
+ this.doPingInstead();
+ return true;
+ } else {
+ this.setupStreamingTimeout(locallyScopedConn);
+ Message sendPacket = ((PreparedQuery)this.query).fillSendPacket(((PreparedQuery)this.query).getQueryBindings());
+ String oldDb = null;
+ if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
+ oldDb = locallyScopedConn.getDatabase();
+ locallyScopedConn.setDatabase(this.getCurrentDatabase());
+ }
- // Walk through the loaded registeredDrivers attempting to make a connection.
- // Remember the first exception that gets raised so we can reraise it.
- SQLException reason = null;
+ CachedResultSetMetaData cachedMetadata = null;
+ boolean cacheResultSetMetadata = (Boolean)locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
+ if (cacheResultSetMetadata) {
+ cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery)this.query).getOriginalSql());
+ }
- for(DriverInfo aDriver : registeredDrivers) {
- // If the caller does not have permission to load the driver then
- // skip it.
- if(isDriverAllowed(aDriver.driver, callerCL)) {
- try {
- // ----------> driver[className=com.mysql.cj.jdbc.Driver@64030b91]
- println(" trying " + aDriver.driver.getClass().getName());
- Connection con = aDriver.driver.connect(url, info);
- if (con != null) {
- // Success!
- println("getConnection returning " + aDriver.driver.getClass().getName());
- return (con);
- }
- } catch (SQLException ex) {
- if (reason == null) {
- reason = ex;
- }
- }
-
- } else {
- println(" skipping: " + aDriver.getClass().getName());
- }
-
- }
+ locallyScopedConn.setSessionMaxRows(this.getQueryInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
+ rs = this.executeInternal(this.maxRows, sendPacket, this.createStreamingResultSet(), this.getQueryInfo().getFirstStmtChar() == 'S', cachedMetadata, false);
+ if (cachedMetadata != null) {
+ locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), cachedMetadata, rs);
+ } else if (rs.hasRows() && cacheResultSetMetadata) {
+ locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery)this.query).getOriginalSql(), (CachedResultSetMetaData)null, rs);
+ }
- // if we got here nobody could connect.
- if (reason != null) {
- println("getConnection failed: " + reason);
- throw reason;
- }
+ if (this.retrieveGeneratedKeys) {
+ rs.setFirstCharOfQuery(this.getQueryInfo().getFirstStmtChar());
+ }
- println("getConnection: no suitable driver found for "+ url);
- throw new SQLException("No suitable driver found for "+ url, "08001");
- }
+ if (oldDb != null) {
+ locallyScopedConn.setDatabase(oldDb);
+ }
+ if (rs != null) {
+ this.lastInsertId = rs.getUpdateID();
+ this.results = rs;
+ }
-上面的driver就是driver[className=com.mysql.cj.jdbc.Driver@64030b91]
-// com.mysql.cj.jdbc.NonRegisteringDriver#connect
-public Connection connect(String url, Properties info) throws SQLException {
- try {
- try {
- if (!ConnectionUrl.acceptsUrl(url)) {
- return null;
- } else {
- ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
- switch (conStr.getType()) {
- case SINGLE_CONNECTION:
- return ConnectionImpl.getInstance(conStr.getMainHost());
- case FAILOVER_CONNECTION:
- case FAILOVER_DNS_SRV_CONNECTION:
- return FailoverConnectionProxy.createProxyInstance(conStr);
- case LOADBALANCE_CONNECTION:
- case LOADBALANCE_DNS_SRV_CONNECTION:
- return LoadBalancedConnectionProxy.createProxyInstance(conStr);
- case REPLICATION_CONNECTION:
- case REPLICATION_DNS_SRV_CONNECTION:
- return ReplicationConnectionProxy.createProxyInstance(conStr);
- default:
- return null;
+ return rs != null && rs.hasRows();
}
}
- } catch (UnsupportedConnectionStringException var5) {
- return null;
- } catch (CJException var6) {
- throw (UnableToConnectException)ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("NonRegisteringDriver.17", new Object[]{var6.toString()}), var6);
}
- } catch (CJException var7) {
- throw SQLExceptionsMapping.translateException(var7);
+ } catch (CJException var11) {
+ throw SQLExceptionsMapping.translateException(var11, this.getExceptionInterceptor());
}
- }
-
-这是个 SINGLE_CONNECTION ,所以调用的就是 return ConnectionImpl.getInstance(conStr.getMainHost());
然后在这里设置了代理类
-public PooledConnection(Connection connection, PooledDataSource dataSource) {
- this.hashCode = connection.hashCode();
- this.realConnection = connection;
- this.dataSource = dataSource;
- this.createdTimestamp = System.currentTimeMillis();
- this.lastUsedTimestamp = System.currentTimeMillis();
- this.valid = true;
- this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
- }
-
-结合这个
-@Override
-public Connection getConnection() throws SQLException {
- return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-}
+ }
-所以最终的connection就是com.mysql.cj.jdbc.ConnectionImpl@358ab600
]]>memcache之类的竞品,但是现在貌似 redis 快一统江湖,这里当然不是在吹,只是个人角度的一个感觉,不权威只是主观感觉。Strings,Lists,Sets,Hashes,Sorted Sets,这五种数据结构先简单介绍下,Strings类型的其实就是我们最常用的 key-value,实际开发中也会用的最多;Lists是列表,这个有些会用来做队列,因为 redis 目前常用的版本支持丰富的列表操作;还有是Sets集合,这个主要的特点就是集合中元素不重复,可以用在有这类需求的场景里;Hashes是叫散列,类似于 Python 中的字典结构;还有就是Sorted Sets这个是个有序集合;一眼看这些其实没啥特别的,除了最后这个有序集合,不过去了解背后的实现方式还是比较有意思的。
-先从Strings开始说,了解过 C 语言的应该知道,C 语言中的字符串其实是个 char[] 字符数组,redis 也不例外,只是最开始的版本就对这个做了一丢丢的优化,而正是这一丢丢的优化,让这个 redis 的使用效率提升了数倍
struct sdshdr {
- // 字符串长度
- int len;
- // 字符串空余字符数
- int free;
- // 字符串内容
- char buf[];
-};
-这里引用了 redis 在 github 上最早的 2.2 版本的代码,代码路径是https://github.com/antirez/redis/blob/2.2/src/sds.h,可以看到这个结构体里只有仨元素,两个 int 型和一个 char 型数组,两个 int 型其实就是我说的优化,因为 C 语言本身的字符串数组,有两个问题,一个是要知道它实际已被占用的长度,需要去遍历这个数组,第二个就是比较容易踩坑的是遍历的时候要注意它有个以\0作为结尾的特点;通过上面的两个 int 型参数,一个是知道字符串目前的长度,一个是知道字符串还剩余多少位空间,这样子坐着两个操作从 O(N)简化到了O(1)了,还有第二个 free 还有个比较重要的作用就是能防止 C 字符串的溢出问题,在存储之前可以先判断 free 长度,如果长度不够就先扩容了,先介绍到这,这个系列可以写蛮多的,慢慢介绍吧
链表是比较常见的数据结构了,但是因为 redis 是用 C 写的,所以在不依赖第三方库的情况下只能自己写一个了,redis 的链表是个有头的链表,而且是无环的,具体的结构我也找了 github 上最早版本的代码
-typedef struct listNode {
- // 前置节点
- struct listNode *prev;
- // 后置节点
- struct listNode *next;
- // 值
- void *value;
-} listNode;
+ nginx 日志小记
+ /2022/04/17/nginx-%E6%97%A5%E5%BF%97%E5%B0%8F%E8%AE%B0/
+ nginx 默认的日志有特定的格式,我们也可以自定义,
+默认的格式是预定义的 combined
+log_format combined '$remote_addr - $remote_user [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '"$http_referer" "$http_user_agent"';
-typedef struct list {
- // 链表表头
- listNode *head;
- // 当前节点,也可以说是最后节点
- listNode *tail;
- // 节点复制函数
- void *(*dup)(void *ptr);
- // 节点值释放函数
- void (*free)(void *ptr);
- // 节点值比较函数
- int (*match)(void *ptr, void *key);
- // 链表包含的节点数量
- unsigned int len;
-} list;
-代码地址是这个https://github.com/antirez/redis/blob/2.2/src/adlist.h
可以看下节点是由listNode承载的,包括值和一个指向前节点跟一个指向后一节点的两个指针,然后值是 void 指针类型,所以可以承载不同类型的值
然后是 list结构用来承载一个链表,包含了表头,和表尾,复制函数,释放函数和比较函数,还有链表长度,因为包含了前两个节点,找到表尾节点跟表头都是 O(1)的时间复杂度,还有节点数量,其实这个跟 SDS 是同一个做法,就是空间换时间,这也是写代码里比较常见的做法,以此让一些高频的操作提速。
字典也是个常用的数据结构,其实只是叫法不同,数据结构中叫 hash 散列,Java 中叫 Map,PHP 中是数组 array,Python 中也叫字典 dict,因为纯 C 语言本身不带这些数据结构,所以这也是个痛并快乐着的过程,享受 C 语言的高性能的同时也要接受它只提供了语言的基本功能的现实,各种轮子都需要自己造,redis 同样实现了自己的字典
下面来看看代码
typedef struct dictEntry {
- void *key;
- void *val;
- struct dictEntry *next;
-} dictEntry;
+配置的日志可以使用这个默认的,如果满足需求的话
+Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
+ access_log off;
+Default: access_log logs/access.log combined;
+Context: http, server, location, if in location, limit_except
-typedef struct dictType {
- unsigned int (*hashFunction)(const void *key);
- void *(*keyDup)(void *privdata, const void *key);
- void *(*valDup)(void *privdata, const void *obj);
- int (*keyCompare)(void *privdata, const void *key1, const void *key2);
- void (*keyDestructor)(void *privdata, void *key);
- void (*valDestructor)(void *privdata, void *obj);
-} dictType;
+而如果需要额外的一些配置的话可以自己定义 log_format ,比如我想要给日志里加上请求时间,那就可以自己定义一个 log_format 比如添加下
+$request_time
+request processing time in seconds with a milliseconds resolution;
+time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client
-/* This is our hash table structure. Every dictionary has two of this as we
- * implement incremental rehashing, for the old to the new table. */
-typedef struct dictht {
- dictEntry **table;
- unsigned long size;
- unsigned long sizemask;
- unsigned long used;
-} dictht;
+log_format combined_extend '$remote_addr - $remote_user [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '"$http_referer" "$http_user_agent" "$request_time"';
-typedef struct dict {
- dictType *type;
- void *privdata;
- dictht ht[2];
- int rehashidx; /* rehashing not in progress if rehashidx == -1 */
- int iterators; /* number of iterators currently running */
-} dict;
-看了下这个 2.2 版本的代码跟最新版的其实也差的不是很多,所以还是照旧用老代码,可以看到上面四个结构体中,其实只有三个是存储数据用的,dictType 是用来放操作函数的,那么三个存放数据的结构体分别是干嘛的,这时候感觉需要一个图来说明比较好,稍等,我去画个图~
这个图看着应该比较清楚这些都是用来干嘛的了,dict 是我们的主体结构,它有一个指向 dictType 的指针,这里面包含了字典的操作函数,然后是一个私有数据指针,接下来是一个 dictht 的数组,包含两个dictht,这个就是用来存数据的了,然后是 rehashidx 表示重哈希的状态,当是-1 的时候表示当前没有重哈希,iterators 表示正在遍历的迭代器的数量。
首先说说为啥需要有两个 dictht,这是因为字典 dict 这个数据结构随着数据量的增减,会需要在中途做扩容或者缩容操作,如果只有一个的话,对它进行扩容缩容时会影响正常的访问和修改操作,或者说保证正常查询,修改的正确性会比较复杂,并且因为需要高效利用空间,不能一下子申请一个非常大的空间来存很少的数据。当 dict 中 dictht 中的数据量超过 size 的时候负载就超过了 1,就需要进行扩容,这里的其实跟 Java 中的 HashMap 比较类似,超过一定的负载之后进行扩容。这里为啥 size 会超过 1 呢,可能有部分不了解这类结构的同学会比较奇怪,其实就是上图中画的,在数据结构中对于散列的冲突有几类解决方法,比如转换成链表,二次散列,找下个空槽等,这里就使用了链表法,或者说拉链法。当一个新元素通过 hashFunction 得出的 key 跟 sizemask 取模之后的值相同了,那就将其放在原来的节点之前,变成链表挂在数组 dictht.table下面,放在原有节点前是考虑到可能会优先访问。
忘了说明下 dictht 跟 dictEntry 的关系了,dictht 就是个哈希表,它里面是个dictEntry 的二维数组,而 dictEntry 是个包含了 key-value 结构之外还有一个 next 指针,因此可以将哈希冲突的以链表的形式保存下来。
在重点说下重哈希,可能同样写 Java 的同学对这个比较有感觉,跟 HashMap 一样,会以 2 的 N 次方进行扩容,那么扩容的方法就会比较简单,每个键重哈希要不就在原来这个槽,要不就在原来的槽加原 dictht.size 的位置;然后是重头戏,具体是怎么做扩容呢,其实这里就把第二个 ht 用上了,其实这两个hashtable 的具体作用有点类似于 jvm 中的两个 survival 区,但是又不全一样,因为 redis 在扩容的时候是采用的渐进式地重哈希,什么叫渐进式的呢,就是它不是像 jvm 那种标记复制的模式直接将一个 eden 区和原来的 survival 区存活的对象复制到另一个 survival 区,而是在每一次添加,删除,查找或者更新操作时,都会额外的帮忙搬运一部分的原 dictht 中的数据,这里会根据 rehashidx 的值来判断,如果是-1 表示并没有在重哈希中,如果是 0 表示开始重哈希了,然后rehashidx 还会随着每次的帮忙搬运往上加,但全部被搬运完成后 rehashidx 又变回了-1,又可以扯到Java 中的 Concurrent HashMap, 他在扩容的时候也使用了类似的操作。
typedef struct intset {
- // 编码方式
- uint32_t encoding;
- // 集合包含的元素数量
- uint32_t length;
- // 保存元素的数组
- int8_t contents[];
-} intset;
+然后其他的比如还有 gzip 压缩,可以设置压缩级别,flush 刷盘时间还有根据条件控制
+这里的条件控制简单看了下还比较厉害
+比如我想对2xx 跟 3xx 的访问不记录日志
+map $status $loggable {
+ ~^[23] 0;
+ default 1;
+}
-/* Note that these encodings are ordered, so:
- * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
-#define INTSET_ENC_INT16 (sizeof(int16_t))
-#define INTSET_ENC_INT32 (sizeof(int32_t))
-#define INTSET_ENC_INT64 (sizeof(int64_t))
-一眼看,为啥整型还需要编码,然后 int8_t 怎么能存下大整形呢,带着这些疑问,我们一步步分析下去,这里的编码其实指的是这个整型集合里存的究竟是多大的整型,16 位,还是 32 位,还是 64 位,结构体下面的宏定义就是表示了 encoding 的可能取值,INTSET_ENC_INT16 表示每个元素用2个字节存储,INTSET_ENC_INT32 表示每个元素用4个字节存储,INTSET_ENC_INT64 表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。length 就是正常的表示集合中元素的数量。最奇怪的应该就是这个 contents 了,是个 int8_t 的数组,那放毛线数据啊,最小的都有 16 位,这里我在看代码和《redis 设计与实现》的时候也有点懵逼,后来查了下发现这是个比较取巧的用法,这里我用自己的理解表述一下,先看看 8,16,32,64 的关系,一眼看就知道都是 2 的 N 次,并且呈两倍关系,而且 8 位刚好一个字节,所以呢其实这里的contents 不是个常规意义上的 int8_t 类型的数组,而是个柔性数组。看下 wiki 的定义
-
-Flexible array members1 were introduced in the C99 standard of the C programming language (in particular, in section §6.7.2.1, item 16, page 103).2 It is a member of a struct, which is an array without a given dimension. It must be the last member of such a struct and it must be accompanied by at least one other member, as in the following example:
-
-struct vectord {
- size_t len;
- double arr[]; // the flexible array member must be last
-};
-在初始化这个 intset 的时候,这个contents数组是不占用空间的,后面的反正用到了申请,那么这里就有一个问题,给出了三种可能的 encoding 值,他们能随便换吗,显然不行,首先在 intset 中数据的存放是有序的,这个有部分原因是方便二分查找,然后存放数据其实随着数据的大小不同会有一个升级的过程,看下图
![]()
新创建的intset只有一个header,总共8个字节。其中encoding = 2, length = 0, 类型都是uint32_t,各占 4 字节,添加15, 5两个元素之后,因为它们是比较小的整数,都能使用2个字节表示,所以encoding不变,值还是2,也就是默认的 INTSET_ENC_INT16,当添加32768的时候,它不再能用2个字节来表示了(2个字节能表达的数据范围是-215~215-1,而32768等于215,超出范围了),因此encoding必须升级到INTSET_ENC_INT32(值为4),即用4个字节表示一个元素。在添加每个元素的过程中,intset始终保持从小到大有序。与ziplist类似,intset也是按小端(little endian)模式存储的(参见维基百科词条Endianness)。比如,在上图中intset添加完所有数据之后,表示encoding字段的4个字节应该解释成0x00000004,而第4个数据应该解释成0x00008000 = 32768
+access_log /path/to/access.log combined if=$loggable;
+
+当 $loggable 是 0 或者空时表示 if 条件为否,上面的默认就是 1,只有当请求状态 status 是 2xx 或 3xx 时才是 0,代表不用记录,有了这个特性就可以更灵活地配置日志
文章主要参考了 nginx 的 log 模块的文档
]]>跳表是个在我们日常的代码中不太常用到的数据结构,相对来讲就没有像数组,链表,字典,散列,树等结构那么熟悉,所以就从头开始分析下,首先是链表,跳表跟链表都有个表字(太硬扯了我🤦♀️),注意这是个有序链表
如上图,在这个链表里如果我要找到 23,是不是我需要从3,5,9开始一直往后找直到找到 23,也就是说时间复杂度是 O(N),N 的一次幂复杂度,那么我们来看看第二个
这个结构跟原先有点不一样,它给链表中偶数位的节点又加了一个指针把它们链接起来,这样子当我们要寻找 23 的时候就可以从原来的一个个往下找变成跳着找,先找到 5,然后是 10,接着是 19,然后是 28,这时候发现 28 比 23 大了,那我在退回到 19,然后从下一层原来的链表往前找,
这里毛估估是不是前面的节点我就少找了一半,有那么点二分法的意思。
前面的其实是跳表的引子,真正的跳表其实不是这样,因为上面的其实有个比较大的问题,就是插入一个元素后需要调整每个元素的指针,在 redis 中的跳表其实是做了个随机层数的优化,因为沿着前面的例子,其实当数据量很大的时候,是不是层数越多,其查询效率越高,但是随着层数变多,要保持这种严格的层数规则其实也会增大处理复杂度,所以 redis 插入每个元素的时候都是使用随机的方式,看一眼代码
/* ZSETs use a specialized version of Skiplists */
-typedef struct zskiplistNode {
- sds ele;
- double score;
- struct zskiplistNode *backward;
- struct zskiplistLevel {
- struct zskiplistNode *forward;
- unsigned long span;
- } level[];
-} zskiplistNode;
+ openresty
+ /2019/06/18/openresty/
+ 目前公司要对一些新的产品功能做灰度测试,因为在后端业务代码层面添加判断比较麻烦,所以想在nginx上做点手脚,就想到了openresty
前后也踩了不少坑,这边先写一点
+首先是日志
error_log logs/error.log debug;
需要nginx开启日志的debug才能看到日志
+使用 lua_code_cache off即可, 另外注意只有使用 content_by_lua_file 才会生效
+http {
+ lua_code_cache off;
+}
-typedef struct zskiplist {
- struct zskiplistNode *header, *tail;
- unsigned long length;
- int level;
-} zskiplist;
+location ~* /(\d+-.*)/api/orgunits/load_all(.*) {
+ default_type 'application/json;charset=utf-8';
+ content_by_lua_file /data/projects/xxx/current/lua/controller/load_data.lua;
+}
-typedef struct zset {
- dict *dict;
- zskiplist *zsl;
-} zset;
-忘了说了,redis 是把 skiplist 跳表用在 zset 里,zset 是个有序的集合,可以看到 zskiplist 就是个跳表的结构,里面用 header 保存跳表的表头,tail 保存表尾,还有长度和最大层级,具体的跳表节点元素使用 zskiplistNode 表示,里面包含了 sds 类型的元素值,double 类型的分值,用来排序,一个 backward 后向指针和一个 zskiplistLevel 数组,每个 level 包含了一个前向指针,和一个 span,span 表示的是跳表前向指针的跨度,这里再补充一点,前面说了为了灵活这个跳表的新增修改,redis 使用了随机层高的方式插入新节点,但是如果所有节点都随机到很高的层级或者所有都很低的话,跳表的效率优势就会减小,所以 redis 使用了个小技巧,贴下代码
-#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
-int zslRandomLevel(void) {
- int level = 1;
- while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
- level += 1;
- return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
-}
-当随机值跟0xFFFF进行与操作小于ZSKIPLIST_P * 0xFFFF时才会增大 level 的值,因此保持了一个相对递减的概率
可以简单分析下,当 random() 的值小于 0xFFFF 的 1/4,才会 level + 1,就意味着当有 1 - 1/4也就是3/4的概率是直接跳出,所以一层的概率是3/4,也就是 1-P,二层的概率是 P*(1-P),三层的概率是 P² * (1-P) 依次递推。
/* The actual Redis Object */
-#define OBJ_STRING 0 /* String object. */
-#define OBJ_LIST 1 /* List object. */
-#define OBJ_SET 2 /* Set object. */
-#define OBJ_ZSET 3 /* Sorted set object. */
-#define OBJ_HASH 4 /* Hash object. */
-/*
- * Objects encoding. Some kind of objects like Strings and Hashes can be
- * internally represented in multiple ways. The 'encoding' field of the object
- * is set to one of this fields for this object. */
-#define OBJ_ENCODING_RAW 0 /* Raw representation */
-#define OBJ_ENCODING_INT 1 /* Encoded as integer */
-#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
-#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
-#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
-#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
-#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
-#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
-#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
-#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
-#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
+使用lua给nginx请求response头添加内容可以用这个
+ngx.header['response'] = 'header'
-#define LRU_BITS 24
-#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
-#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
-#define OBJ_SHARED_REFCOUNT INT_MAX
-typedef struct redisObject {
- unsigned type:4;
- unsigned encoding:4;
- unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
- * LFU data (least significant 8 bits frequency
- * and most significant 16 bits access time). */
- int refcount;
- void *ptr;
-} robj;
-主体结构就是这个 redisObject,
-后续:
+一开始在本地环境的时候使用content_by_lua_file只关注了头,后来发到测试环境发现请求内容都没代理转发到后端服务上
网上查了下发现content_by_lua_file是将请求的所有内容包括response都用这里面的lua脚本生成了,content这个词就表示是请求内容
后来改成了access_by_lua_file就正常了,只是要去获取请求内容和修改响应头,并不是要完整的接管请求
后来又碰到了一个坑是nginx有个client_body_buffer_size的配置参数,nginx在32位和64位系统里有8K和16K两个默认值,当请求内容大于这两个值的时候,会把请求内容放到临时文件里,这个时候openresty里的ngx.req.get_post_args()就会报“failed to get post args: requesty body in temp file not supported”这个错误,将client_body_buffer_size这个参数配置调大一点就好了
+还有就是lua的异常捕获,网上看一般是用pcall和xpcall来进行保护调用,因为问题主要出在cjson的decode,这里有两个解决方案,一个就是将cjson.decode使用pcall封装,
+local decode = require("cjson").decode
+
+function json_decode( str )
+ local ok, t = pcall(decode, str)
+ if not ok then
+ return nil
+ end
+
+ return t
+end
+ 这个是使用了pcall,称为保护调用,会在内部错误后返回两个参数,第一个是false,第二个是错误信息
还有一种是使用cjson.safe包
local json = require("cjson.safe")
+local str = [[ {"key:"value"} ]]
+
+local t = json.decode(str)
+if t then
+ ngx.say(" --> ", type(t))
+end
+cjson.safe包会在解析失败的时候返回nil
+还有一个是redis链接时如果host使用的是域名的话会提示“failed to connect: no resolver defined to resolve “redis.xxxxxx.com””,这里需要使用nginx的resolver指令,
resolver 8.8.8.8 valid=3600s;
还有一点补充下
就是业务在使用redis的时候使用了db的特性,所以在lua访问redis的时候也需要执行db,这里lua的redis库也支持了这个特性,可以使用instance:select(config:get(‘db’))来切换db
发现一个不错的openresty站点
地址
// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
-private transient volatile Node head;
-
-// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
-private transient volatile Node tail;
+ mybatis系列-sql 类的简要分析
+ /2023/03/19/mybatis%E7%B3%BB%E5%88%97-sql-%E7%B1%BB%E7%9A%84%E7%AE%80%E8%A6%81%E5%88%86%E6%9E%90/
+ 上次就比较简单的讲了使用,这块也比较简单,因为封装得不是很复杂,首先我们从 select 作为入口来看看,这个具体的实现,
+String selectSql = new SQL() {{
+ SELECT("id", "name");
+ FROM("student");
+ WHERE("id = #{id}");
+ }}.toString();
+SELECT 方法的实现,
+public T SELECT(String... columns) {
+ sql().statementType = SQLStatement.StatementType.SELECT;
+ sql().select.addAll(Arrays.asList(columns));
+ return getSelf();
+}
+statementType是个枚举
+public enum StatementType {
+ DELETE, INSERT, SELECT, UPDATE
+}
+那这个就是个 select 语句,然后会把参数转成 list 添加到 select 变量里,
然后是 from 语句,这个大概也能猜到就是设置下表名,
+public T FROM(String table) {
+ sql().tables.add(table);
+ return getSelf();
+}
+往 tables 里添加了 table,这个 tables 是什么呢
这里也可以看下所有的变量,
+StatementType statementType;
+List<String> sets = new ArrayList<>();
+List<String> select = new ArrayList<>();
+List<String> tables = new ArrayList<>();
+List<String> join = new ArrayList<>();
+List<String> innerJoin = new ArrayList<>();
+List<String> outerJoin = new ArrayList<>();
+List<String> leftOuterJoin = new ArrayList<>();
+List<String> rightOuterJoin = new ArrayList<>();
+List<String> where = new ArrayList<>();
+List<String> having = new ArrayList<>();
+List<String> groupBy = new ArrayList<>();
+List<String> orderBy = new ArrayList<>();
+List<String> lastList = new ArrayList<>();
+List<String> columns = new ArrayList<>();
+List<List<String>> valuesList = new ArrayList<>();
+可以看到是一堆 List 先暂存这些sql 片段,然后再拼装成 sql 语句,
因为它重写了 toString 方法
+@Override
+public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sql().sql(sb);
+ return sb.toString();
+}
+调用的 sql 方法是
+public String sql(Appendable a) {
+ SafeAppendable builder = new SafeAppendable(a);
+ if (statementType == null) {
+ return null;
+ }
-// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
-// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
-private volatile int state;
+ String answer;
-// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
-// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
-// if (currentThread == getExclusiveOwnerThread()) {state++}
-private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
-大概了解了 aqs 底层的双向等待队列,
结构是这样的
![]()
每个 node 里面主要是的代码结构也比较简单
-static final class Node {
- // 标识节点当前在共享模式下
- static final Node SHARED = new Node();
- // 标识节点当前在独占模式下
- static final Node EXCLUSIVE = null;
+ switch (statementType) {
+ case DELETE:
+ answer = deleteSQL(builder);
+ break;
- // ======== 下面的几个int常量是给waitStatus用的 ===========
- /** waitStatus value to indicate thread has cancelled */
- // 代码此线程取消了争抢这个锁
- static final int CANCELLED = 1;
- /** waitStatus value to indicate successor's thread needs unparking */
- // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
- static final int SIGNAL = -1;
- /** waitStatus value to indicate thread is waiting on condition */
- // 本文不分析condition,所以略过吧,下一篇文章会介绍这个
- static final int CONDITION = -2;
- /**
- * waitStatus value to indicate the next acquireShared should
- * unconditionally propagate
- */
- // 同样的不分析,略过吧
- static final int PROPAGATE = -3;
- // =====================================================
+ case INSERT:
+ answer = insertSQL(builder);
+ break;
+ case SELECT:
+ answer = selectSQL(builder);
+ break;
- // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
- // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
- // ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
- volatile int waitStatus;
- // 前驱节点的引用
- volatile Node prev;
- // 后继节点的引用
- volatile Node next;
- // 这个就是线程本尊
- volatile Thread thread;
+ case UPDATE:
+ answer = updateSQL(builder);
+ break;
-}
-其实可以主要关注这个 waitStatus 因为这个是后面的节点给前面的节点设置的,等于-1 的时候代表后面有节点等待,需要去唤醒,
这里使用了一个变种的 CLH 队列实现,CLH 队列相关内容可以查看这篇 自旋锁、排队自旋锁、MCS锁、CLH锁
+ default:
+ answer = null;
+ }
+
+ return answer;
+ }
+根据上面的 statementType判断是个什么 sql,我们这个是 selectSQL 就走的 SELECT 这个分支
+private String selectSQL(SafeAppendable builder) {
+ if (distinct) {
+ sqlClause(builder, "SELECT DISTINCT", select, "", "", ", ");
+ } else {
+ sqlClause(builder, "SELECT", select, "", "", ", ");
+ }
+
+ sqlClause(builder, "FROM", tables, "", "", ", ");
+ joins(builder);
+ sqlClause(builder, "WHERE", where, "(", ")", " AND ");
+ sqlClause(builder, "GROUP BY", groupBy, "", "", ", ");
+ sqlClause(builder, "HAVING", having, "(", ")", " AND ");
+ sqlClause(builder, "ORDER BY", orderBy, "", "", ", ");
+ limitingRowsStrategy.appendClause(builder, offset, limit);
+ return builder.toString();
+}
+上面的可以看出来就是按我们常规的 sql 理解顺序来处理
就是select ... from ... where ... 这样子
再看下 sqlClause 的代码
private void sqlClause(SafeAppendable builder, String keyword, List<String> parts, String open, String close,
+ String conjunction) {
+ if (!parts.isEmpty()) {
+ if (!builder.isEmpty()) {
+ builder.append("\n");
+ }
+ builder.append(keyword);
+ builder.append(" ");
+ builder.append(open);
+ String last = "________";
+ for (int i = 0, n = parts.size(); i < n; i++) {
+ String part = parts.get(i);
+ if (i > 0 && !part.equals(AND) && !part.equals(OR) && !last.equals(AND) && !last.equals(OR)) {
+ builder.append(conjunction);
+ }
+ builder.append(part);
+ last = part;
+ }
+ builder.append(close);
+ }
+ }
+这里的拼接方式还需要判断 AND 和 OR 的判断逻辑,其他就没什么特别的了,只是where 语句中的 lastList 不知道是干嘛的,好像只有添加跟赋值的操作,有知道的大神也可以评论指导下
]]>/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
- * We use bit fields keep the quicklistNode at 32 bytes.
- * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
- * encoding: 2 bits, RAW=1, LZF=2.
- * container: 2 bits, NONE=1, ZIPLIST=2.
- * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
- * attempted_compress: 1 bit, boolean, used for verifying during testing.
- * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
-typedef struct quicklistNode {
- struct quicklistNode *prev;
- struct quicklistNode *next;
- unsigned char *zl;
- unsigned int sz; /* ziplist size in bytes */
- unsigned int count : 16; /* count of items in ziplist */
- unsigned int encoding : 2; /* RAW==1 or LZF==2 */
- unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
- unsigned int recompress : 1; /* was this node previous compressed? */
- unsigned int attempted_compress : 1; /* node can't compress; too small */
- unsigned int extra : 10; /* more bits to steal for future usage */
-} quicklistNode;
-
-/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
- * 'sz' is byte length of 'compressed' field.
- * 'compressed' is LZF data with total (compressed) length 'sz'
- * NOTE: uncompressed length is stored in quicklistNode->sz.
- * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
-typedef struct quicklistLZF {
- unsigned int sz; /* LZF size in bytes*/
- char compressed[];
-} quicklistLZF;
+ pcre-intro-and-a-simple-package
+ /2015/01/16/pcre-intro-and-a-simple-package/
+ Pcre
+Perl Compatible Regular Expressions (PCRE) is a regular
expression C library inspired by the regular expression
capabilities in the Perl programming language, written
by Philip Hazel, starting in summer 1997.
+
+因为最近工作内容的一部分需要做字符串的识别处理,所以就顺便用上了之前在PHP中用过的正则,在C/C++中本身不包含正则库,这里使用的pcre,对MFC开发,在这里提供了静态链接库,在引入lib跟.h文件后即可使用。
+
-/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
- * 'count' is the number of total entries.
- * 'len' is the number of quicklist nodes.
- * 'compress' is: -1 if compression disabled, otherwise it's the number
- * of quicklistNodes to leave uncompressed at ends of quicklist.
- * 'fill' is the user-requested (or default) fill factor. */
-typedef struct quicklist {
- quicklistNode *head;
- quicklistNode *tail;
- unsigned long count; /* total count of all entries in all ziplists */
- unsigned long len; /* number of quicklistNodes */
- int fill : 16; /* fill factor for individual nodes */
- unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
-} quicklist;
-粗略看下,quicklist 里有 head,tail, quicklistNode里有 prev,next 指针,是不是有链表的基本轮廓了,那么为啥这玩意要称为快表呢,快在哪,关键就在这个unsigned char *zl;zl 是不是前面又看到过,就是 ziplist ,这是什么鬼,链表里用压缩表,这不套娃么,先别急,回顾下前面说的 ziplist,ziplist 有哪些特点,内存利用率高,可以从表头快速定位到尾节点,节点可以从后往前找,但是有个缺点,就是从中间插入的效率比较低,需要整体往后移,这个其实是普通数组的优化版,但还是有数组的一些劣势,所以要真的快,是不是可以将链表跟数组真的结合起来。
这里有两个 redis 的配置参数,list-max-ziplist-size 和 list-compress-depth,先来说第一个,既然快表是将链表跟压缩表数组结合起来使用,那么具体怎么用呢,比如我有一个 10 个元素的 list,那具体怎么放,每个 quicklistNode 里放多大的 ziplist,假如每个快表节点的 ziplist 只放一个元素,那么其实这就退化成了一个链表,如果 10 个元素放在一个 quicklistNode 的 ziplist 里,那就退化成了一个 ziplist,所以有了这个 list-max-ziplist-size,而且它还比较牛,能取正负值,当是正值时,对应的就是每个 quicklistNode 的 ziplist 中的元素个数,比如配置了 list-max-ziplist-size = 5,那么我刚才的 10 个元素的 list 就是一个两个 quicklistNode 组成的快表,每个 quicklistNode 中的 ziplist 包含了五个元素,当 list-max-ziplist-size取负值的时候,它限制了 ziplist 的字节数
size_t offset = (-fill) - 1;
-if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
- if (sz <= optimization_level[offset]) {
- return 1;
- } else {
- return 0;
+Regular Expression Syntax
然后是一些正则语法,官方的语法文档比较科学严谨,特别是对类似于贪婪匹配等细节的说明,当然一般的使用可以在网上找到很多匹配语法,例如这个。
+PCRE函数介绍
+pcre_compile
原型:
+
+#include <pcre.h>
+pcre *pcre_compile(const char *pattern, int options, const char **errptr, int *erroffset, const unsigned char *tableptr);
+功能:将一个正则表达式编译成一个内部表示,在匹配多个字符串时,可以加速匹配。其同pcre_compile2功能一样只是缺少一个参数errorcodeptr。
参数:
pattern 正则表达式
options 为0,或者其他参数选项
errptr 出错消息
erroffset 出错位置
tableptr 指向一个字符数组的指针,可以设置为空NULL
+
+pcre_exec
原型:
+
+#include <pcre.h>
+int pcre_exec(const pcre *code, const pcre_extra *extra, const char *subject, int length, int startoffset, int options, int *ovector, int ovecsize)
+功能:使用编译好的模式进行匹配,采用与Perl相似的算法,返回匹配串的偏移位置。
参数:
code 编译好的模式
extra 指向一个pcre_extra结构体,可以为NULL
subject 需要匹配的字符串
length 匹配的字符串长度(Byte)
startoffset 匹配的开始位置
options 选项位
ovector 指向一个结果的整型数组
ovecsize 数组大小。
+这里是两个最常用的函数的简单说明,pcre的静态库提供了一系列的函数以供使用,可以参考这个博客说明,另外对于以上函数的具体参数详细说明可以参考官网此处
+一个丑陋的封装
void COcxDemoDlg::pcre_exec_all(const pcre * re, PCRE_SPTR src, vector<pair<int, int>> &vc)
+{
+ int rc;
+ int ovector[30];
+ int i = 0;
+ pair<int, int> pr;
+ rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
+ for (; rc > 0;)
+ {
+ i = ovector[1];
+ pr.first = ovector[2];
+ pr.second = ovector[3];
+ vc.push_back(pr);
+ rc = pcre_exec(re, NULL, src, strlen(src), i, 0, ovector, 30);
+ }
+}
+vector中是全文匹配后的索引对,只是简单地用下。
+]]><?php
+interface int1{
+ const INTER1 = 111;
+ function inter1();
+}
+interface int2{
+ const INTER1 = 222;
+ function inter2();
+}
+abstract class abst1{
+ public function abstr1(){
+ echo 1111;
+ }
+ abstract function abstra1(){
+ echo 'ahahahha';
}
-} else {
- return 0;
}
-
-/* Optimization levels for size-based filling */
-static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
-
-/* Create a new quicklist.
- * Free with quicklistRelease(). */
-quicklist *quicklistCreate(void) {
- struct quicklist *quicklist;
-
- quicklist = zmalloc(sizeof(*quicklist));
- quicklist->head = quicklist->tail = NULL;
- quicklist->len = 0;
- quicklist->count = 0;
- quicklist->compress = 0;
- quicklist->fill = -2;
- return quicklist;
-}
-这个 fill 就是传进来的 list-max-ziplist-size, 具体对应的就是
quicklist->fill = -2;list-compress-depth这个参数呢是用来配置压缩的,等等压缩是为啥,不是里面已经是压缩表了么,大牛们就是为了性能殚精竭虑,这里考虑到的是一个场景,一般状况下,list 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示
/* depth of end nodes not to compress;0=off */
-简单说下插入元素的过程
-/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
-void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
- int where) {
- if (where == QUICKLIST_HEAD) {
- quicklistPushHead(quicklist, value, sz);
- } else if (where == QUICKLIST_TAIL) {
- quicklistPushTail(quicklist, value, sz);
+abstract class abst2{
+ public function abstr2(){
+ echo 1111;
}
+ abstract function abstra2();
}
+class normal1 extends abst1{
+ protected function abstr2(){
+ echo 222;
+ }
+}
-/* Add new entry to head node of quicklist.
- *
- * Returns 0 if used existing head.
- * Returns 1 if new head created. */
-int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
- quicklistNode *orig_head = quicklist->head;
- if (likely(
- _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
- quicklist->head->zl =
- ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
- quicklistNodeUpdateSz(quicklist->head);
- } else {
- quicklistNode *node = quicklistCreateNode();
- node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
-
- quicklistNodeUpdateSz(node);
- _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
- }
- quicklist->count++;
- quicklist->head->count++;
- return (orig_head != quicklist->head);
-}
-
-/* Add new entry to tail node of quicklist.
- *
- * Returns 0 if used existing tail.
- * Returns 1 if new tail created. */
-int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
- quicklistNode *orig_tail = quicklist->tail;
- if (likely(
- _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
- quicklist->tail->zl =
- ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
- quicklistNodeUpdateSz(quicklist->tail);
- } else {
- quicklistNode *node = quicklistCreateNode();
- node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
-
- quicklistNodeUpdateSz(node);
- _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
- }
- quicklist->count++;
- quicklist->tail->count++;
- return (orig_tail != quicklist->tail);
-}
-
-/* Wrappers for node inserting around existing node. */
-REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
- quicklistNode *old_node,
- quicklistNode *new_node) {
- __quicklistInsertNode(quicklist, old_node, new_node, 0);
-}
-
-REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
- quicklistNode *old_node,
- quicklistNode *new_node) {
- __quicklistInsertNode(quicklist, old_node, new_node, 1);
-}
-
-/* Insert 'new_node' after 'old_node' if 'after' is 1.
- * Insert 'new_node' before 'old_node' if 'after' is 0.
- * Note: 'new_node' is *always* uncompressed, so if we assign it to
- * head or tail, we do not need to uncompress it. */
-REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
- quicklistNode *old_node,
- quicklistNode *new_node, int after) {
- if (after) {
- new_node->prev = old_node;
- if (old_node) {
- new_node->next = old_node->next;
- if (old_node->next)
- old_node->next->prev = new_node;
- old_node->next = new_node;
- }
- if (quicklist->tail == old_node)
- quicklist->tail = new_node;
- } else {
- new_node->next = old_node;
- if (old_node) {
- new_node->prev = old_node->prev;
- if (old_node->prev)
- old_node->prev->next = new_node;
- old_node->prev = new_node;
- }
- if (quicklist->head == old_node)
- quicklist->head = new_node;
- }
- /* If this insert creates the only element so far, initialize head/tail. */
- if (quicklist->len == 0) {
- quicklist->head = quicklist->tail = new_node;
- }
-
- if (old_node)
- quicklistCompress(quicklist, old_node);
+PHP Fatal error: Abstract function abst1::abstra1() cannot contain body in new.php on line 17
- quicklist->len++;
-}
-前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。
+Fatal error: Abstract function abst1::abstra1() cannot contain body in php on line 17 ]]>--The ziplist is a specially encoded dually linked list that is designed to be very memory efficient.
-
这是摘自 redis 源码中ziplist.c 文件的注释,也说明了原因,它的大概结构是这样子
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
-其中<zlbytes>表示 ziplist 占用的字节总数,类型是uint32_t,32 位的无符号整型,当然表示的字节数也包含自己本身占用的 4 个<zltail> 类型也是是uint32_t,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。<zltail>的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。<uint16_t zllen> 表示ziplist 中的数据项个数,因为是 16 位,所以当数量超过所能表示的最大的数量,它的 16 位全会置为 1,但是真实的数量需要遍历整个 ziplist 才能知道<entry>是具体的数据项,后面解释<zlend> ziplist 的最后一个字节,固定是255。
再看一下<entry>中的具体结构,
<prevlen> <encoding> <entry-data>
-首先这个<prevlen>有两种情况,一种是前面的元素的长度,如果是小于等于 253的时候就用一个uint8_t 来表示前一元素的长度,如果大于的话他将占用五个字节,第一个字节是 254,即表示这个字节已经表示不下了,需要后面的四个字节帮忙表示<encoding>这个就比较复杂,把源码的注释放下面先看下
* |00pppppp| - 1 byte
-* String value with length less than or equal to 63 bytes (6 bits).
-* "pppppp" represents the unsigned 6 bit length.
-* |01pppppp|qqqqqqqq| - 2 bytes
-* String value with length less than or equal to 16383 bytes (14 bits).
-* IMPORTANT: The 14 bit number is stored in big endian.
-* |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
-* String value with length greater than or equal to 16384 bytes.
-* Only the 4 bytes following the first byte represents the length
-* up to 32^2-1. The 6 lower bits of the first byte are not used and
-* are set to zero.
-* IMPORTANT: The 32 bit number is stored in big endian.
-* |11000000| - 3 bytes
-* Integer encoded as int16_t (2 bytes).
-* |11010000| - 5 bytes
-* Integer encoded as int32_t (4 bytes).
-* |11100000| - 9 bytes
-* Integer encoded as int64_t (8 bytes).
-* |11110000| - 4 bytes
-* Integer encoded as 24 bit signed (3 bytes).
-* |11111110| - 2 bytes
-* Integer encoded as 8 bit signed (1 byte).
-* |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
-* Unsigned integer from 0 to 12. The encoded value is actually from
-* 1 to 13 because 0000 and 1111 can not be used, so 1 should be
-* subtracted from the encoded 4 bit value to obtain the right value.
-* |11111111| - End of ziplist special entry.
-首先如果 encoding 的前两位是 00 的话代表这个元素是个 6 位的字符串,即直接将数据保存在 encoding 中,不消耗额外的<entry-data>,如果前两位是 01 的话表示是个 14 位的字符串,如果是 10 的话表示encoding 块之后的四个字节是存放字符串类型的数据,encoding 的剩余 6 位置 0。
如果 encoding 的前两位是 11 的话表示这是个整型,具体的如果后两位是00的话,表示后面是个2字节的 int16_t 类型,如果是01的话,后面是个4字节的int32_t,如果是10的话后面是8字节的int64_t,如果是 11 的话后面是 3 字节的有符号整型,这些都要最后 4 位都是 0 的情况噢
剩下当是11111110时,则表示是一个1 字节的有符号数,如果是 1111xxxx,其中xxxx在0000 到 1101 表示实际的 1 到 13,为啥呢,因为 0000 前面已经用过了,而 1110 跟 1111 也都有用了。
看个具体的例子(上下有点对不齐,将就看)
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
-|**zlbytes***| |***zltail***| |*zllen*| |entry1 entry2| |zlend|
-第一部分代表整个 ziplist 有 15 个字节,zlbytes 自己占了 4 个 zltail 表示最后一个元素的偏移量,第 13 个字节起,zllen 表示有 2 个元素,第一个元素是00f3,00表示前一个元素长度是 0,本来前面就没元素(不过不知道这个能不能优化这一字节),然后是 f3,换成二进制就是11110011,对照上面的注释,是落在|1111xxxx|这个类型里,注意这个其实是用 0001 到 1101 也就是 1到 13 来表示 0到 12,所以 f3 应该就是 2,第一个元素是 2,第二个元素呢,02 代表前一个元素也就是刚才说的这个,占用 2 字节,f6 展开也是刚才的类型,实际是 5,ff 表示 ziplist 的结尾,所以这个 ziplist 里面是两个元素,2 跟 5
变量命名类似于php
+PS C:\Users\Nicks> $a=1
+PS C:\Users\Nicks> $b=2
+PS C:\Users\Nicks> $a*$b
+2
+有一个比较好用的是变量交换
一般的语言做两个变量交换一般需要一个临时变量
$tmp=$a
+$a=$b
+$b=$tmp
+而在powershell中可以这样
+$a,$b=$b,$a
+PS C:\Users\Nicks> $a,$b=$b,$a
+PS C:\Users\Nicks> $a
+2
+PS C:\Users\Nicks> $b
+1
+还可以通过这个
+PS C:\Users\Nicks> ls variable:
+
+Name Value
+---- -----
+$ $b
+? True
+^ $b
+a 2
+args {}
+b 1
+查看现存的变量
当然一般脚本都是动态类型的,
可以通过
gettype方法
| Policy | -Description | -
|---|---|
| noeviction 不逐出 | -Returns an error if the memory limit has been reached when trying to insert more data,插入更多数据时,如果内存达到上限了,返回错误 | -
| allkeys-lru 所有的 key 使用 lru 逐出 | -Evicts the least recently used keys out of all keys 在所有 key 中逐出最近最少使用的 | -
| allkeys-lfu 所有的 key 使用 lfu 逐出 | -Evicts the least frequently used keys out of all keys 在所有 key 中逐出最近最不频繁使用的 | -
| allkeys-random 所有的 key 中随机逐出 | -Randomly evicts keys out of all keys 在所有 key 中随机逐出 | -
| volatile-lru | -Evicts the least recently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lru 策略逐出 | -
| volatile-lfu | -Evicts the least frequently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lfu 策略逐出 | -
| volatile-random | -Randomly evicts keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中随机逐出 | -
| volatile-ttl | -Evicts the shortest time-to-live keys out of all keys with an “expire” field set.在设置了过期时间的 key 空间 expire 中逐出更早过期的 | -
而在这其中默认使用的策略是 volatile-lru,对 lru 跟 lfu 想有更多的了解可以看下我之前的文章redis系列介绍八-淘汰策略
+fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。这个操作其实可以类比为写屏障,正常的读取是没问题的,当有写入时就会分裂。
+1、减少分配和复制资源时带来的瞬时延迟;
2、减少不必要的资源分配;
CopyOnWrite的缺点:
1、如果父子进程都需要进行大量的写操作,会产生大量的分页错误(页异常中断page-fault);
Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。这里其实更巧妙了,在细节上去优化会产生大量页异常中断的情况。
redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
主要代码是
/* This function is called when we are going to perform some operation
- * in a given key, but such key may be already logically expired even if
- * it still exists in the database. The main way this function is called
- * is via lookupKey*() family of functions.
- *
- * The behavior of the function depends on the replication role of the
- * instance, because slave instances do not expire keys, they wait
- * for DELs from the master for consistency matters. However even
- * slaves will try to have a coherent return value for the function,
- * so that read commands executed in the slave side will be able to
- * behave like if the key is expired even if still present (because the
- * master has yet to propagate the DEL).
- *
- * In masters as a side effect of finding a key which is expired, such
- * key will be evicted from the database. Also this may trigger the
- * propagation of a DEL/UNLINK command in AOF / replication stream.
- *
- * The return value of the function is 0 if the key is still valid,
- * otherwise the function returns 1 if the key is expired. */
-int expireIfNeeded(redisDb *db, robj *key) {
- if (!keyIsExpired(db,key)) return 0;
-
- /* If we are running in the context of a slave, instead of
- * evicting the expired key from the database, we return ASAP:
- * the slave key expiration is controlled by the master that will
- * send us synthesized DEL operations for expired keys.
- *
- * Still we try to return the right information to the caller,
- * that is, 0 if we think the key should be still valid, 1 if
- * we think the key is expired at this time. */
- if (server.masterhost != NULL) return 1;
-
- /* Delete the key */
- server.stat_expiredkeys++;
- propagateExpire(db,key,server.lazyfree_lazy_expire);
- notifyKeyspaceEvent(NOTIFY_EXPIRED,
- "expired",key,db->id);
- return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
- dbSyncDelete(db,key);
-}
-
-/* Check if the key is expired. */
-int keyIsExpired(redisDb *db, robj *key) {
- mstime_t when = getExpire(db,key);
- mstime_t now;
+ rabbitmq-tips
+ /2017/04/25/rabbitmq-tips/
+ rabbitmq 介绍接触了一下rabbitmq,原来在选型的时候是在rabbitmq跟kafka之间做选择,网上搜了一下之后发现kafka的优势在于吞吐量,而rabbitmq相对注重可靠性,因为应用在im上,需要保证消息不能丢失所以就暂时选定rabbitmq,
Message Queue的需求由来已久,80年代最早在金融交易中,高盛等公司采用Teknekron公司的产品,当时的Message queuing软件叫做:the information bus(TIB)。 TIB被电信和通讯公司采用,路透社收购了Teknekron公司。之后,IBM开发了MQSeries,微软开发了Microsoft Message Queue(MSMQ)。这些商业MQ供应商的问题是厂商锁定,价格高昂。2001年,Java Message queuing试图解决锁定和交互性的问题,但对应用来说反而更加麻烦了。
RabbitMQ采用Erlang语言开发。Erlang语言由Ericson设计,专门为开发concurrent和distribution系统的一种语言,在电信领域使用广泛。OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件/库/工具,如mnesia/SASL,极大方便了Erlang应用的开发。OTP就类似于Python语言中众多的module,用户借助这些module可以很方便的开发应用。
于是2004年,摩根大通和iMatrix开始着手Advanced Message Queuing Protocol (AMQP)开放标准的开发。2006年,AMQP规范发布。2007年,Rabbit技术公司基于AMQP标准开发的RabbitMQ 1.0 发布。所有主要的编程语言均有与代理接口通讯的客户端库。
+简单的使用经验
通俗的理解
这里介绍下其中的一些概念,connection表示和队列服务器的连接,一般情况下是tcp连接, channel表示通道,可以在一个连接上建立多个通道,这里主要是节省了tcp连接握手的成本,exchange可以理解成一个路由器,将消息推送给对应的队列queue,其实是像一个订阅的模式。
+集群经验
rabbitmqctl stop这个是关闭rabbitmq,在搭建集群时候先关闭服务,然后使用rabbitmq-server -detached静默启动,这时候使用rabbitmqctl cluster_status查看集群状态,因为还没将节点加入集群,所以只能看到类似
+Cluster status of node rabbit@rabbit1 ...
+[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]},
+ {running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}]
+...done.
+然后就可以把当前节点加入集群,
+rabbit2$ rabbitmqctl stop_app #这个stop_app与stop的区别是前者停的是rabbitmq应用,保留erlang节点,
+ #后者是停止了rabbitmq和erlang节点
+Stopping node rabbit@rabbit2 ...done.
+rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1 #这里可以用--ram指定将当前节点作为内存节点加入集群
+Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done.
+rabbit2$ rabbitmqctl start_app
+Starting node rabbit@rabbit2 ...done.
+其他可以参考官方文档
+一些坑
消息丢失
这里碰到过一个坑,对于使用exchange来做消息路由的,会有一个情况,就是在routing_key没被订阅的时候,会将该条找不到路由对应的queue的消息丢掉What happens if we break our contract and send a message with one or four words, like "orange" or "quick.orange.male.rabbit"? Well, these messages won't match any bindings and will be lost.对应链接,而当使用空的exchange时,会保留消息,当出现消费者的时候就可以将收到之前生产者所推送的消息对应链接,这里就是用了空的exchange。
+集群搭建
集群搭建的时候有个erlang vm生成的random cookie,这个是用来做集群之间认证的,相同的cookie才能连接,但是如果通过vim打开复制后在其他几点新建文件写入会多一个换行,导致集群建立是报错,所以这里最好使用scp等传输命令直接传输cookie文件,同时要注意下cookie的文件权限。
另外在集群搭建的时候如果更改过hostname,那么要把rabbitmq的数据库删除,否则启动后会马上挂掉
+]]>
+
+ php
+
+
+ php
+ mq
+ im
+
+ memcache之类的竞品,但是现在貌似 redis 快一统江湖,这里当然不是在吹,只是个人角度的一个感觉,不权威只是主观感觉。Strings,Lists,Sets,Hashes,Sorted Sets,这五种数据结构先简单介绍下,Strings类型的其实就是我们最常用的 key-value,实际开发中也会用的最多;Lists是列表,这个有些会用来做队列,因为 redis 目前常用的版本支持丰富的列表操作;还有是Sets集合,这个主要的特点就是集合中元素不重复,可以用在有这类需求的场景里;Hashes是叫散列,类似于 Python 中的字典结构;还有就是Sorted Sets这个是个有序集合;一眼看这些其实没啥特别的,除了最后这个有序集合,不过去了解背后的实现方式还是比较有意思的。
+先从Strings开始说,了解过 C 语言的应该知道,C 语言中的字符串其实是个 char[] 字符数组,redis 也不例外,只是最开始的版本就对这个做了一丢丢的优化,而正是这一丢丢的优化,让这个 redis 的使用效率提升了数倍
struct sdshdr {
+ // 字符串长度
+ int len;
+ // 字符串空余字符数
+ int free;
+ // 字符串内容
+ char buf[];
+};
+这里引用了 redis 在 github 上最早的 2.2 版本的代码,代码路径是https://github.com/antirez/redis/blob/2.2/src/sds.h,可以看到这个结构体里只有仨元素,两个 int 型和一个 char 型数组,两个 int 型其实就是我说的优化,因为 C 语言本身的字符串数组,有两个问题,一个是要知道它实际已被占用的长度,需要去遍历这个数组,第二个就是比较容易踩坑的是遍历的时候要注意它有个以\0作为结尾的特点;通过上面的两个 int 型参数,一个是知道字符串目前的长度,一个是知道字符串还剩余多少位空间,这样子坐着两个操作从 O(N)简化到了O(1)了,还有第二个 free 还有个比较重要的作用就是能防止 C 字符串的溢出问题,在存储之前可以先判断 free 长度,如果长度不够就先扩容了,先介绍到这,这个系列可以写蛮多的,慢慢介绍吧
链表是比较常见的数据结构了,但是因为 redis 是用 C 写的,所以在不依赖第三方库的情况下只能自己写一个了,redis 的链表是个有头的链表,而且是无环的,具体的结构我也找了 github 上最早版本的代码
+typedef struct listNode {
+ // 前置节点
+ struct listNode *prev;
+ // 后置节点
+ struct listNode *next;
+ // 值
+ void *value;
+} listNode;
- if (when < 0) return 0; /* No expire for this key */
+typedef struct list {
+ // 链表表头
+ listNode *head;
+ // 当前节点,也可以说是最后节点
+ listNode *tail;
+ // 节点复制函数
+ void *(*dup)(void *ptr);
+ // 节点值释放函数
+ void (*free)(void *ptr);
+ // 节点值比较函数
+ int (*match)(void *ptr, void *key);
+ // 链表包含的节点数量
+ unsigned int len;
+} list;
+代码地址是这个https://github.com/antirez/redis/blob/2.2/src/adlist.h
可以看下节点是由listNode承载的,包括值和一个指向前节点跟一个指向后一节点的两个指针,然后值是 void 指针类型,所以可以承载不同类型的值
然后是 list结构用来承载一个链表,包含了表头,和表尾,复制函数,释放函数和比较函数,还有链表长度,因为包含了前两个节点,找到表尾节点跟表头都是 O(1)的时间复杂度,还有节点数量,其实这个跟 SDS 是同一个做法,就是空间换时间,这也是写代码里比较常见的做法,以此让一些高频的操作提速。
字典也是个常用的数据结构,其实只是叫法不同,数据结构中叫 hash 散列,Java 中叫 Map,PHP 中是数组 array,Python 中也叫字典 dict,因为纯 C 语言本身不带这些数据结构,所以这也是个痛并快乐着的过程,享受 C 语言的高性能的同时也要接受它只提供了语言的基本功能的现实,各种轮子都需要自己造,redis 同样实现了自己的字典
下面来看看代码
typedef struct dictEntry {
+ void *key;
+ void *val;
+ struct dictEntry *next;
+} dictEntry;
- /* Don't expire anything while loading. It will be done later. */
- if (server.loading) return 0;
+typedef struct dictType {
+ unsigned int (*hashFunction)(const void *key);
+ void *(*keyDup)(void *privdata, const void *key);
+ void *(*valDup)(void *privdata, const void *obj);
+ int (*keyCompare)(void *privdata, const void *key1, const void *key2);
+ void (*keyDestructor)(void *privdata, void *key);
+ void (*valDestructor)(void *privdata, void *obj);
+} dictType;
- /* If we are in the context of a Lua script, we pretend that time is
- * blocked to when the Lua script started. This way a key can expire
- * only the first time it is accessed and not in the middle of the
- * script execution, making propagation to slaves / AOF consistent.
- * See issue #1525 on Github for more information. */
- if (server.lua_caller) {
- now = server.lua_time_start;
- }
- /* If we are in the middle of a command execution, we still want to use
- * a reference time that does not change: in that case we just use the
- * cached time, that we update before each call in the call() function.
- * This way we avoid that commands such as RPOPLPUSH or similar, that
- * may re-open the same key multiple times, can invalidate an already
- * open object in a next call, if the next call will see the key expired,
- * while the first did not. */
- else if (server.fixed_time_expire > 0) {
- now = server.mstime;
- }
- /* For the other cases, we want to use the most fresh time we have. */
- else {
- now = mstime();
- }
+/* This is our hash table structure. Every dictionary has two of this as we
+ * implement incremental rehashing, for the old to the new table. */
+typedef struct dictht {
+ dictEntry **table;
+ unsigned long size;
+ unsigned long sizemask;
+ unsigned long used;
+} dictht;
- /* The key expired if the current (virtual or real) time is greater
- * than the expire time of the key. */
- return now > when;
-}
-/* Return the expire time of the specified key, or -1 if no expire
- * is associated with this key (i.e. the key is non volatile) */
-long long getExpire(redisDb *db, robj *key) {
- dictEntry *de;
+typedef struct dict {
+ dictType *type;
+ void *privdata;
+ dictht ht[2];
+ int rehashidx; /* rehashing not in progress if rehashidx == -1 */
+ int iterators; /* number of iterators currently running */
+} dict;
+看了下这个 2.2 版本的代码跟最新版的其实也差的不是很多,所以还是照旧用老代码,可以看到上面四个结构体中,其实只有三个是存储数据用的,dictType 是用来放操作函数的,那么三个存放数据的结构体分别是干嘛的,这时候感觉需要一个图来说明比较好,稍等,我去画个图~
这个图看着应该比较清楚这些都是用来干嘛的了,dict 是我们的主体结构,它有一个指向 dictType 的指针,这里面包含了字典的操作函数,然后是一个私有数据指针,接下来是一个 dictht 的数组,包含两个dictht,这个就是用来存数据的了,然后是 rehashidx 表示重哈希的状态,当是-1 的时候表示当前没有重哈希,iterators 表示正在遍历的迭代器的数量。
首先说说为啥需要有两个 dictht,这是因为字典 dict 这个数据结构随着数据量的增减,会需要在中途做扩容或者缩容操作,如果只有一个的话,对它进行扩容缩容时会影响正常的访问和修改操作,或者说保证正常查询,修改的正确性会比较复杂,并且因为需要高效利用空间,不能一下子申请一个非常大的空间来存很少的数据。当 dict 中 dictht 中的数据量超过 size 的时候负载就超过了 1,就需要进行扩容,这里的其实跟 Java 中的 HashMap 比较类似,超过一定的负载之后进行扩容。这里为啥 size 会超过 1 呢,可能有部分不了解这类结构的同学会比较奇怪,其实就是上图中画的,在数据结构中对于散列的冲突有几类解决方法,比如转换成链表,二次散列,找下个空槽等,这里就使用了链表法,或者说拉链法。当一个新元素通过 hashFunction 得出的 key 跟 sizemask 取模之后的值相同了,那就将其放在原来的节点之前,变成链表挂在数组 dictht.table下面,放在原有节点前是考虑到可能会优先访问。
忘了说明下 dictht 跟 dictEntry 的关系了,dictht 就是个哈希表,它里面是个dictEntry 的二维数组,而 dictEntry 是个包含了 key-value 结构之外还有一个 next 指针,因此可以将哈希冲突的以链表的形式保存下来。
在重点说下重哈希,可能同样写 Java 的同学对这个比较有感觉,跟 HashMap 一样,会以 2 的 N 次方进行扩容,那么扩容的方法就会比较简单,每个键重哈希要不就在原来这个槽,要不就在原来的槽加原 dictht.size 的位置;然后是重头戏,具体是怎么做扩容呢,其实这里就把第二个 ht 用上了,其实这两个hashtable 的具体作用有点类似于 jvm 中的两个 survival 区,但是又不全一样,因为 redis 在扩容的时候是采用的渐进式地重哈希,什么叫渐进式的呢,就是它不是像 jvm 那种标记复制的模式直接将一个 eden 区和原来的 survival 区存活的对象复制到另一个 survival 区,而是在每一次添加,删除,查找或者更新操作时,都会额外的帮忙搬运一部分的原 dictht 中的数据,这里会根据 rehashidx 的值来判断,如果是-1 表示并没有在重哈希中,如果是 0 表示开始重哈希了,然后rehashidx 还会随着每次的帮忙搬运往上加,但全部被搬运完成后 rehashidx 又变回了-1,又可以扯到Java 中的 Concurrent HashMap, 他在扩容的时候也使用了类似的操作。
typedef struct intset {
+ // 编码方式
+ uint32_t encoding;
+ // 集合包含的元素数量
+ uint32_t length;
+ // 保存元素的数组
+ int8_t contents[];
+} intset;
- /* No expire? return ASAP */
- if (dictSize(db->expires) == 0 ||
- (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
+/* Note that these encodings are ordered, so:
+ * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
+#define INTSET_ENC_INT16 (sizeof(int16_t))
+#define INTSET_ENC_INT32 (sizeof(int32_t))
+#define INTSET_ENC_INT64 (sizeof(int64_t))
+一眼看,为啥整型还需要编码,然后 int8_t 怎么能存下大整形呢,带着这些疑问,我们一步步分析下去,这里的编码其实指的是这个整型集合里存的究竟是多大的整型,16 位,还是 32 位,还是 64 位,结构体下面的宏定义就是表示了 encoding 的可能取值,INTSET_ENC_INT16 表示每个元素用2个字节存储,INTSET_ENC_INT32 表示每个元素用4个字节存储,INTSET_ENC_INT64 表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。length 就是正常的表示集合中元素的数量。最奇怪的应该就是这个 contents 了,是个 int8_t 的数组,那放毛线数据啊,最小的都有 16 位,这里我在看代码和《redis 设计与实现》的时候也有点懵逼,后来查了下发现这是个比较取巧的用法,这里我用自己的理解表述一下,先看看 8,16,32,64 的关系,一眼看就知道都是 2 的 N 次,并且呈两倍关系,而且 8 位刚好一个字节,所以呢其实这里的contents 不是个常规意义上的 int8_t 类型的数组,而是个柔性数组。看下 wiki 的定义
+++Flexible array members1 were introduced in the C99 standard of the C programming language (in particular, in section §6.7.2.1, item 16, page 103).2 It is a member of a struct, which is an array without a given dimension. It must be the last member of such a struct and it must be accompanied by at least one other member, as in the following example:
+
struct vectord {
+ size_t len;
+ double arr[]; // the flexible array member must be last
+};
+在初始化这个 intset 的时候,这个contents数组是不占用空间的,后面的反正用到了申请,那么这里就有一个问题,给出了三种可能的 encoding 值,他们能随便换吗,显然不行,首先在 intset 中数据的存放是有序的,这个有部分原因是方便二分查找,然后存放数据其实随着数据的大小不同会有一个升级的过程,看下图
新创建的intset只有一个header,总共8个字节。其中encoding = 2, length = 0, 类型都是uint32_t,各占 4 字节,添加15, 5两个元素之后,因为它们是比较小的整数,都能使用2个字节表示,所以encoding不变,值还是2,也就是默认的 INTSET_ENC_INT16,当添加32768的时候,它不再能用2个字节来表示了(2个字节能表达的数据范围是-215~215-1,而32768等于215,超出范围了),因此encoding必须升级到INTSET_ENC_INT32(值为4),即用4个字节表示一个元素。在添加每个元素的过程中,intset始终保持从小到大有序。与ziplist类似,intset也是按小端(little endian)模式存储的(参见维基百科词条Endianness)。比如,在上图中intset添加完所有数据之后,表示encoding字段的4个字节应该解释成0x00000004,而第4个数据应该解释成0x00008000 = 32768
跳表是个在我们日常的代码中不太常用到的数据结构,相对来讲就没有像数组,链表,字典,散列,树等结构那么熟悉,所以就从头开始分析下,首先是链表,跳表跟链表都有个表字(太硬扯了我🤦♀️),注意这是个有序链表
如上图,在这个链表里如果我要找到 23,是不是我需要从3,5,9开始一直往后找直到找到 23,也就是说时间复杂度是 O(N),N 的一次幂复杂度,那么我们来看看第二个
这个结构跟原先有点不一样,它给链表中偶数位的节点又加了一个指针把它们链接起来,这样子当我们要寻找 23 的时候就可以从原来的一个个往下找变成跳着找,先找到 5,然后是 10,接着是 19,然后是 28,这时候发现 28 比 23 大了,那我在退回到 19,然后从下一层原来的链表往前找,
这里毛估估是不是前面的节点我就少找了一半,有那么点二分法的意思。
前面的其实是跳表的引子,真正的跳表其实不是这样,因为上面的其实有个比较大的问题,就是插入一个元素后需要调整每个元素的指针,在 redis 中的跳表其实是做了个随机层数的优化,因为沿着前面的例子,其实当数据量很大的时候,是不是层数越多,其查询效率越高,但是随着层数变多,要保持这种严格的层数规则其实也会增大处理复杂度,所以 redis 插入每个元素的时候都是使用随机的方式,看一眼代码
/* ZSETs use a specialized version of Skiplists */
+typedef struct zskiplistNode {
+ sds ele;
+ double score;
+ struct zskiplistNode *backward;
+ struct zskiplistLevel {
+ struct zskiplistNode *forward;
+ unsigned long span;
+ } level[];
+} zskiplistNode;
- /* The entry was found in the expire dict, this means it should also
- * be present in the main dict (safety check). */
- serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
- return dictGetSignedIntegerVal(de);
-}
-这里有几点要注意的,第一是当惰性删除时会根据lazyfree_lazy_expire这个参数去判断是执行同步删除还是异步删除,另外一点是对于 slave,是不需要执行的,因为会在 master 过期时向 slave 发送 del 指令。
光采用这个策略会有什么问题呢,假如一些key 一直未被访问,那这些 key 就不会过期了,导致一直被占用着内存,所以 redis 采取了懒汉式过期加定期过期策略,定期策略是怎么执行的呢
/* This function handles 'background' operations we are required to do
- * incrementally in Redis databases, such as active key expiring, resizing,
- * rehashing. */
-void databasesCron(void) {
- /* Expire keys by random sampling. Not required for slaves
- * as master will synthesize DELs for us. */
- if (server.active_expire_enabled) {
- if (server.masterhost == NULL) {
- activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
- } else {
- expireSlaveKeys();
- }
- }
+typedef struct zskiplist {
+ struct zskiplistNode *header, *tail;
+ unsigned long length;
+ int level;
+} zskiplist;
- /* Defrag keys gradually. */
- activeDefragCycle();
+typedef struct zset {
+ dict *dict;
+ zskiplist *zsl;
+} zset;
+忘了说了,redis 是把 skiplist 跳表用在 zset 里,zset 是个有序的集合,可以看到 zskiplist 就是个跳表的结构,里面用 header 保存跳表的表头,tail 保存表尾,还有长度和最大层级,具体的跳表节点元素使用 zskiplistNode 表示,里面包含了 sds 类型的元素值,double 类型的分值,用来排序,一个 backward 后向指针和一个 zskiplistLevel 数组,每个 level 包含了一个前向指针,和一个 span,span 表示的是跳表前向指针的跨度,这里再补充一点,前面说了为了灵活这个跳表的新增修改,redis 使用了随机层高的方式插入新节点,但是如果所有节点都随机到很高的层级或者所有都很低的话,跳表的效率优势就会减小,所以 redis 使用了个小技巧,贴下代码
+#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
+int zslRandomLevel(void) {
+ int level = 1;
+ while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
+ level += 1;
+ return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
+}
+当随机值跟0xFFFF进行与操作小于ZSKIPLIST_P * 0xFFFF时才会增大 level 的值,因此保持了一个相对递减的概率
可以简单分析下,当 random() 的值小于 0xFFFF 的 1/4,才会 level + 1,就意味着当有 1 - 1/4也就是3/4的概率是直接跳出,所以一层的概率是3/4,也就是 1-P,二层的概率是 P*(1-P),三层的概率是 P² * (1-P) 依次递推。
/* The actual Redis Object */
+#define OBJ_STRING 0 /* String object. */
+#define OBJ_LIST 1 /* List object. */
+#define OBJ_SET 2 /* Set object. */
+#define OBJ_ZSET 3 /* Sorted set object. */
+#define OBJ_HASH 4 /* Hash object. */
+/*
+ * Objects encoding. Some kind of objects like Strings and Hashes can be
+ * internally represented in multiple ways. The 'encoding' field of the object
+ * is set to one of this fields for this object. */
+#define OBJ_ENCODING_RAW 0 /* Raw representation */
+#define OBJ_ENCODING_INT 1 /* Encoded as integer */
+#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
+#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
+#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
+#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
+#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
+#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
+#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
+#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
+#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
- /* Perform hash tables rehashing if needed, but only if there are no
- * other processes saving the DB on disk. Otherwise rehashing is bad
- * as will cause a lot of copy-on-write of memory pages. */
- if (!hasActiveChildProcess()) {
- /* We use global counters so if we stop the computation at a given
- * DB we'll be able to start from the successive in the next
- * cron loop iteration. */
- static unsigned int resize_db = 0;
- static unsigned int rehash_db = 0;
- int dbs_per_call = CRON_DBS_PER_CALL;
- int j;
+#define LRU_BITS 24
+#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
+#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
- /* Don't test more DBs than we have. */
- if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
+#define OBJ_SHARED_REFCOUNT INT_MAX
+typedef struct redisObject {
+ unsigned type:4;
+ unsigned encoding:4;
+ unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
+ * LFU data (least significant 8 bits frequency
+ * and most significant 16 bits access time). */
+ int refcount;
+ void *ptr;
+} robj;
+主体结构就是这个 redisObject,
+/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
+ * We use bit fields keep the quicklistNode at 32 bytes.
+ * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
+ * encoding: 2 bits, RAW=1, LZF=2.
+ * container: 2 bits, NONE=1, ZIPLIST=2.
+ * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
+ * attempted_compress: 1 bit, boolean, used for verifying during testing.
+ * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
+typedef struct quicklistNode {
+ struct quicklistNode *prev;
+ struct quicklistNode *next;
+ unsigned char *zl;
+ unsigned int sz; /* ziplist size in bytes */
+ unsigned int count : 16; /* count of items in ziplist */
+ unsigned int encoding : 2; /* RAW==1 or LZF==2 */
+ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
+ unsigned int recompress : 1; /* was this node previous compressed? */
+ unsigned int attempted_compress : 1; /* node can't compress; too small */
+ unsigned int extra : 10; /* more bits to steal for future usage */
+} quicklistNode;
- /* Resize */
- for (j = 0; j < dbs_per_call; j++) {
- tryResizeHashTables(resize_db % server.dbnum);
- resize_db++;
- }
+/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
+ * 'sz' is byte length of 'compressed' field.
+ * 'compressed' is LZF data with total (compressed) length 'sz'
+ * NOTE: uncompressed length is stored in quicklistNode->sz.
+ * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
+typedef struct quicklistLZF {
+ unsigned int sz; /* LZF size in bytes*/
+ char compressed[];
+} quicklistLZF;
- /* Rehash */
- if (server.activerehashing) {
- for (j = 0; j < dbs_per_call; j++) {
- int work_done = incrementallyRehash(rehash_db);
- if (work_done) {
- /* If the function did some work, stop here, we'll do
- * more at the next cron loop. */
- break;
- } else {
- /* If this db didn't need rehash, we'll try the next one. */
- rehash_db++;
- rehash_db %= server.dbnum;
- }
- }
- }
+/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
+ * 'count' is the number of total entries.
+ * 'len' is the number of quicklist nodes.
+ * 'compress' is: -1 if compression disabled, otherwise it's the number
+ * of quicklistNodes to leave uncompressed at ends of quicklist.
+ * 'fill' is the user-requested (or default) fill factor. */
+typedef struct quicklist {
+ quicklistNode *head;
+ quicklistNode *tail;
+ unsigned long count; /* total count of all entries in all ziplists */
+ unsigned long len; /* number of quicklistNodes */
+ int fill : 16; /* fill factor for individual nodes */
+ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
+} quicklist;
+粗略看下,quicklist 里有 head,tail, quicklistNode里有 prev,next 指针,是不是有链表的基本轮廓了,那么为啥这玩意要称为快表呢,快在哪,关键就在这个unsigned char *zl;zl 是不是前面又看到过,就是 ziplist ,这是什么鬼,链表里用压缩表,这不套娃么,先别急,回顾下前面说的 ziplist,ziplist 有哪些特点,内存利用率高,可以从表头快速定位到尾节点,节点可以从后往前找,但是有个缺点,就是从中间插入的效率比较低,需要整体往后移,这个其实是普通数组的优化版,但还是有数组的一些劣势,所以要真的快,是不是可以将链表跟数组真的结合起来。
这里有两个 redis 的配置参数,list-max-ziplist-size 和 list-compress-depth,先来说第一个,既然快表是将链表跟压缩表数组结合起来使用,那么具体怎么用呢,比如我有一个 10 个元素的 list,那具体怎么放,每个 quicklistNode 里放多大的 ziplist,假如每个快表节点的 ziplist 只放一个元素,那么其实这就退化成了一个链表,如果 10 个元素放在一个 quicklistNode 的 ziplist 里,那就退化成了一个 ziplist,所以有了这个 list-max-ziplist-size,而且它还比较牛,能取正负值,当是正值时,对应的就是每个 quicklistNode 的 ziplist 中的元素个数,比如配置了 list-max-ziplist-size = 5,那么我刚才的 10 个元素的 list 就是一个两个 quicklistNode 组成的快表,每个 quicklistNode 中的 ziplist 包含了五个元素,当 list-max-ziplist-size取负值的时候,它限制了 ziplist 的字节数
size_t offset = (-fill) - 1;
+if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
+ if (sz <= optimization_level[offset]) {
+ return 1;
+ } else {
+ return 0;
}
+} else {
+ return 0;
}
-/* Try to expire a few timed out keys. The algorithm used is adaptive and
- * will use few CPU cycles if there are few expiring keys, otherwise
- * it will get more aggressive to avoid that too much memory is used by
- * keys that can be removed from the keyspace.
- *
- * Every expire cycle tests multiple databases: the next call will start
- * again from the next db, with the exception of exists for time limit: in that
- * case we restart again from the last database we were processing. Anyway
- * no more than CRON_DBS_PER_CALL databases are tested at every iteration.
- *
- * The function can perform more or less work, depending on the "type"
- * argument. It can execute a "fast cycle" or a "slow cycle". The slow
- * cycle is the main way we collect expired cycles: this happens with
- * the "server.hz" frequency (usually 10 hertz).
- *
- * However the slow cycle can exit for timeout, since it used too much time.
- * For this reason the function is also invoked to perform a fast cycle
- * at every event loop cycle, in the beforeSleep() function. The fast cycle
- * will try to perform less work, but will do it much more often.
- *
- * The following are the details of the two expire cycles and their stop
- * conditions:
- *
- * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
- * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
- * microseconds, and is not repeated again before the same amount of time.
- * The cycle will also refuse to run at all if the latest slow cycle did not
- * terminate because of a time limit condition.
- *
- * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
- * executed, where the time limit is a percentage of the REDIS_HZ period
- * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
- * fast cycle, the check of every database is interrupted once the number
- * of already expired keys in the database is estimated to be lower than
- * a given percentage, in order to avoid doing too much work to gain too
- * little memory.
- *
- * The configured expire "effort" will modify the baseline parameters in
- * order to do more work in both the fast and slow expire cycles.
- */
-
-#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
-#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
-#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
-#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
- we do extra efforts. */
-void activeExpireCycle(int type) {
- /* Adjust the running parameters according to the configured expire
- * effort. The default effort is 1, and the maximum configurable effort
- * is 10. */
- unsigned long
- effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
- config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
- ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
- config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
- ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
- config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
- 2*effort,
- config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
- effort;
- /* This function has some global state in order to continue the work
- * incrementally across calls. */
- static unsigned int current_db = 0; /* Last DB tested. */
- static int timelimit_exit = 0; /* Time limit hit in previous call? */
- static long long last_fast_cycle = 0; /* When last fast cycle ran. */
+/* Optimization levels for size-based filling */
+static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};
- int j, iteration = 0;
- int dbs_per_call = CRON_DBS_PER_CALL;
- long long start = ustime(), timelimit, elapsed;
+/* Create a new quicklist.
+ * Free with quicklistRelease(). */
+quicklist *quicklistCreate(void) {
+ struct quicklist *quicklist;
- /* When clients are paused the dataset should be static not just from the
- * POV of clients not being able to write, but also from the POV of
- * expires and evictions of keys not being performed. */
- if (clientsArePaused()) return;
-
- if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
- /* Don't start a fast cycle if the previous cycle did not exit
- * for time limit, unless the percentage of estimated stale keys is
- * too high. Also never repeat a fast cycle for the same period
- * as the fast cycle total duration itself. */
- if (!timelimit_exit &&
- server.stat_expired_stale_perc < config_cycle_acceptable_stale)
- return;
-
- if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
- return;
-
- last_fast_cycle = start;
+ quicklist = zmalloc(sizeof(*quicklist));
+ quicklist->head = quicklist->tail = NULL;
+ quicklist->len = 0;
+ quicklist->count = 0;
+ quicklist->compress = 0;
+ quicklist->fill = -2;
+ return quicklist;
+}
+这个 fill 就是传进来的 list-max-ziplist-size, 具体对应的就是
quicklist->fill = -2;list-compress-depth这个参数呢是用来配置压缩的,等等压缩是为啥,不是里面已经是压缩表了么,大牛们就是为了性能殚精竭虑,这里考虑到的是一个场景,一般状况下,list 都是两端的访问频率比较高,那么是不是可以对中间的数据进行压缩,那么这个参数就是用来表示
/* depth of end nodes not to compress;0=off */
+简单说下插入元素的过程
+/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
+void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
+ int where) {
+ if (where == QUICKLIST_HEAD) {
+ quicklistPushHead(quicklist, value, sz);
+ } else if (where == QUICKLIST_TAIL) {
+ quicklistPushTail(quicklist, value, sz);
}
+}
- /* We usually should test CRON_DBS_PER_CALL per iteration, with
- * two exceptions:
- *
- * 1) Don't test more DBs than we have.
- * 2) If last time we hit the time limit, we want to scan all DBs
- * in this iteration, as there is work to do in some DB and we don't want
- * expired keys to use memory for too much time. */
- if (dbs_per_call > server.dbnum || timelimit_exit)
- dbs_per_call = server.dbnum;
-
- /* We can use at max 'config_cycle_slow_time_perc' percentage of CPU
- * time per iteration. Since this function gets called with a frequency of
- * server.hz times per second, the following is the max amount of
- * microseconds we can spend in this function. */
- timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
- timelimit_exit = 0;
- if (timelimit <= 0) timelimit = 1;
-
- if (type == ACTIVE_EXPIRE_CYCLE_FAST)
- timelimit = config_cycle_fast_duration; /* in microseconds. */
-
- /* Accumulate some global stats as we expire keys, to have some idea
- * about the number of keys that are already logically expired, but still
- * existing inside the database. */
- long total_sampled = 0;
- long total_expired = 0;
-
- for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
- /* Expired and checked in a single loop. */
- unsigned long expired, sampled;
-
- redisDb *db = server.db+(current_db % server.dbnum);
-
- /* Increment the DB now so we are sure if we run out of time
- * in the current DB we'll restart from the next. This allows to
- * distribute the time evenly across DBs. */
- current_db++;
-
- /* Continue to expire if at the end of the cycle more than 25%
- * of the keys were expired. */
- do {
- unsigned long num, slots;
- long long now, ttl_sum;
- int ttl_samples;
- iteration++;
-
- /* If there is nothing to expire try next DB ASAP. */
- if ((num = dictSize(db->expires)) == 0) {
- db->avg_ttl = 0;
- break;
- }
- slots = dictSlots(db->expires);
- now = mstime();
-
- /* When there are less than 1% filled slots, sampling the key
- * space is expensive, so stop here waiting for better times...
- * The dictionary will be resized asap. */
- if (num && slots > DICT_HT_INITIAL_SIZE &&
- (num*100/slots < 1)) break;
-
- /* The main collection cycle. Sample random keys among keys
- * with an expire set, checking for expired ones. */
- expired = 0;
- sampled = 0;
- ttl_sum = 0;
- ttl_samples = 0;
-
- if (num > config_keys_per_loop)
- num = config_keys_per_loop;
+/* Add new entry to head node of quicklist.
+ *
+ * Returns 0 if used existing head.
+ * Returns 1 if new head created. */
+int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
+ quicklistNode *orig_head = quicklist->head;
+ if (likely(
+ _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
+ quicklist->head->zl =
+ ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
+ quicklistNodeUpdateSz(quicklist->head);
+ } else {
+ quicklistNode *node = quicklistCreateNode();
+ node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
- /* Here we access the low level representation of the hash table
- * for speed concerns: this makes this code coupled with dict.c,
- * but it hardly changed in ten years.
- *
- * Note that certain places of the hash table may be empty,
- * so we want also a stop condition about the number of
- * buckets that we scanned. However scanning for free buckets
- * is very fast: we are in the cache line scanning a sequential
- * array of NULL pointers, so we can scan a lot more buckets
- * than keys in the same time. */
- long max_buckets = num*20;
- long checked_buckets = 0;
+ quicklistNodeUpdateSz(node);
+ _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
+ }
+ quicklist->count++;
+ quicklist->head->count++;
+ return (orig_head != quicklist->head);
+}
- while (sampled < num && checked_buckets < max_buckets) {
- for (int table = 0; table < 2; table++) {
- if (table == 1 && !dictIsRehashing(db->expires)) break;
+/* Add new entry to tail node of quicklist.
+ *
+ * Returns 0 if used existing tail.
+ * Returns 1 if new tail created. */
+int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
+ quicklistNode *orig_tail = quicklist->tail;
+ if (likely(
+ _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
+ quicklist->tail->zl =
+ ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
+ quicklistNodeUpdateSz(quicklist->tail);
+ } else {
+ quicklistNode *node = quicklistCreateNode();
+ node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
- unsigned long idx = db->expires_cursor;
- idx &= db->expires->ht[table].sizemask;
- dictEntry *de = db->expires->ht[table].table[idx];
- long long ttl;
+ quicklistNodeUpdateSz(node);
+ _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
+ }
+ quicklist->count++;
+ quicklist->tail->count++;
+ return (orig_tail != quicklist->tail);
+}
- /* Scan the current bucket of the current table. */
- checked_buckets++;
- while(de) {
- /* Get the next entry now since this entry may get
- * deleted. */
- dictEntry *e = de;
- de = de->next;
+/* Wrappers for node inserting around existing node. */
+REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
+ quicklistNode *old_node,
+ quicklistNode *new_node) {
+ __quicklistInsertNode(quicklist, old_node, new_node, 0);
+}
- ttl = dictGetSignedIntegerVal(e)-now;
- if (activeExpireCycleTryExpire(db,e,now)) expired++;
- if (ttl > 0) {
- /* We want the average TTL of keys yet
- * not expired. */
- ttl_sum += ttl;
- ttl_samples++;
- }
- sampled++;
- }
- }
- db->expires_cursor++;
- }
- total_expired += expired;
- total_sampled += sampled;
+REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
+ quicklistNode *old_node,
+ quicklistNode *new_node) {
+ __quicklistInsertNode(quicklist, old_node, new_node, 1);
+}
- /* Update the average TTL stats for this database. */
- if (ttl_samples) {
- long long avg_ttl = ttl_sum/ttl_samples;
+/* Insert 'new_node' after 'old_node' if 'after' is 1.
+ * Insert 'new_node' before 'old_node' if 'after' is 0.
+ * Note: 'new_node' is *always* uncompressed, so if we assign it to
+ * head or tail, we do not need to uncompress it. */
+REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
+ quicklistNode *old_node,
+ quicklistNode *new_node, int after) {
+ if (after) {
+ new_node->prev = old_node;
+ if (old_node) {
+ new_node->next = old_node->next;
+ if (old_node->next)
+ old_node->next->prev = new_node;
+ old_node->next = new_node;
+ }
+ if (quicklist->tail == old_node)
+ quicklist->tail = new_node;
+ } else {
+ new_node->next = old_node;
+ if (old_node) {
+ new_node->prev = old_node->prev;
+ if (old_node->prev)
+ old_node->prev->next = new_node;
+ old_node->prev = new_node;
+ }
+ if (quicklist->head == old_node)
+ quicklist->head = new_node;
+ }
+ /* If this insert creates the only element so far, initialize head/tail. */
+ if (quicklist->len == 0) {
+ quicklist->head = quicklist->tail = new_node;
+ }
- /* Do a simple running average with a few samples.
- * We just use the current estimate with a weight of 2%
- * and the previous estimate with a weight of 98%. */
- if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
- db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
- }
+ if (old_node)
+ quicklistCompress(quicklist, old_node);
- /* We can't block forever here even if there are many keys to
- * expire. So after a given amount of milliseconds return to the
- * caller waiting for the other active expire cycle. */
- if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
- elapsed = ustime()-start;
- if (elapsed > timelimit) {
- timelimit_exit = 1;
- server.stat_expired_time_cap_reached_count++;
- break;
- }
- }
- /* We don't repeat the cycle for the current database if there are
- * an acceptable amount of stale keys (logically expired but yet
- * not reclained). */
- } while ((expired*100/sampled) > config_cycle_acceptable_stale);
- }
-
- elapsed = ustime()-start;
- server.stat_expire_cycle_time_used += elapsed;
- latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
-
- /* Update our estimate of keys existing but yet to be expired.
- * Running average with this sample accounting for 5%. */
- double current_perc;
- if (total_sampled) {
- current_perc = (double)total_expired/total_sampled;
- } else
- current_perc = 0;
- server.stat_expired_stale_perc = (current_perc*0.05)+
- (server.stat_expired_stale_perc*0.95);
-}
-执行定期清除分成两种类型,快和慢,分别由beforeSleep和databasesCron调用,快版有两个限制,一个是执行时长由ACTIVE_EXPIRE_CYCLE_FAST_DURATION限制,另一个是执行间隔是 2 倍的ACTIVE_EXPIRE_CYCLE_FAST_DURATION,另外这还可以由配置的server.active_expire_effort参数来控制,默认是 1,最大是 10
onfig_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
- ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort
-然后会从一定数量的 db 中找出一定数量的带过期时间的 key(保存在 expires中),这里的数量是由
-config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
- ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort
-```
-控制,慢速的执行时长是
-```C
-config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
- 2*effort
-timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
-这里还有一个额外的退出条件,如果当前数据库的抽样结果已经达到我们所允许的过期 key 百分比,则下次不再处理当前 db,继续处理下个 db
+ quicklist->len++; +} +前面第一步先根据插入的是头还是尾选择不同的 push 函数,quicklistPushHead 或者 quicklistPushTail,举例分析下从头插入的 quicklistPushHead,先判断当前的 quicklistNode 节点还能不能允许再往 ziplist 里添加元素,如果可以就添加,如果不允许就新建一个 quicklistNode,然后调用 _quicklistInsertNodeBefore 将节点插进去,具体插入quicklist节点的操作类似链表的插入。
]]>变量命名类似于php
-PS C:\Users\Nicks> $a=1
-PS C:\Users\Nicks> $b=2
-PS C:\Users\Nicks> $a*$b
-2
-有一个比较好用的是变量交换
一般的语言做两个变量交换一般需要一个临时变量
$tmp=$a
-$a=$b
-$b=$tmp
-而在powershell中可以这样
-$a,$b=$b,$a
-PS C:\Users\Nicks> $a,$b=$b,$a
-PS C:\Users\Nicks> $a
-2
-PS C:\Users\Nicks> $b
-1
-还可以通过这个
-PS C:\Users\Nicks> ls variable:
-
-Name Value
----- -----
-$ $b
-? True
-^ $b
-a 2
-args {}
-b 1
-查看现存的变量
当然一般脚本都是动态类型的,
可以通过
gettype方法
++The ziplist is a specially encoded dually linked list that is designed to be very memory efficient.
+
这是摘自 redis 源码中ziplist.c 文件的注释,也说明了原因,它的大概结构是这样子
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
+其中<zlbytes>表示 ziplist 占用的字节总数,类型是uint32_t,32 位的无符号整型,当然表示的字节数也包含自己本身占用的 4 个<zltail> 类型也是是uint32_t,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。<zltail>的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。<uint16_t zllen> 表示ziplist 中的数据项个数,因为是 16 位,所以当数量超过所能表示的最大的数量,它的 16 位全会置为 1,但是真实的数量需要遍历整个 ziplist 才能知道<entry>是具体的数据项,后面解释<zlend> ziplist 的最后一个字节,固定是255。
再看一下<entry>中的具体结构,
<prevlen> <encoding> <entry-data>
+首先这个<prevlen>有两种情况,一种是前面的元素的长度,如果是小于等于 253的时候就用一个uint8_t 来表示前一元素的长度,如果大于的话他将占用五个字节,第一个字节是 254,即表示这个字节已经表示不下了,需要后面的四个字节帮忙表示<encoding>这个就比较复杂,把源码的注释放下面先看下
* |00pppppp| - 1 byte
+* String value with length less than or equal to 63 bytes (6 bits).
+* "pppppp" represents the unsigned 6 bit length.
+* |01pppppp|qqqqqqqq| - 2 bytes
+* String value with length less than or equal to 16383 bytes (14 bits).
+* IMPORTANT: The 14 bit number is stored in big endian.
+* |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
+* String value with length greater than or equal to 16384 bytes.
+* Only the 4 bytes following the first byte represents the length
+* up to 32^2-1. The 6 lower bits of the first byte are not used and
+* are set to zero.
+* IMPORTANT: The 32 bit number is stored in big endian.
+* |11000000| - 3 bytes
+* Integer encoded as int16_t (2 bytes).
+* |11010000| - 5 bytes
+* Integer encoded as int32_t (4 bytes).
+* |11100000| - 9 bytes
+* Integer encoded as int64_t (8 bytes).
+* |11110000| - 4 bytes
+* Integer encoded as 24 bit signed (3 bytes).
+* |11111110| - 2 bytes
+* Integer encoded as 8 bit signed (1 byte).
+* |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
+* Unsigned integer from 0 to 12. The encoded value is actually from
+* 1 to 13 because 0000 and 1111 can not be used, so 1 should be
+* subtracted from the encoded 4 bit value to obtain the right value.
+* |11111111| - End of ziplist special entry.
+首先如果 encoding 的前两位是 00 的话代表这个元素是个 6 位的字符串,即直接将数据保存在 encoding 中,不消耗额外的<entry-data>,如果前两位是 01 的话表示是个 14 位的字符串,如果是 10 的话表示encoding 块之后的四个字节是存放字符串类型的数据,encoding 的剩余 6 位置 0。
如果 encoding 的前两位是 11 的话表示这是个整型,具体的如果后两位是00的话,表示后面是个2字节的 int16_t 类型,如果是01的话,后面是个4字节的int32_t,如果是10的话后面是8字节的int64_t,如果是 11 的话后面是 3 字节的有符号整型,这些都要最后 4 位都是 0 的情况噢
剩下当是11111110时,则表示是一个1 字节的有符号数,如果是 1111xxxx,其中xxxx在0000 到 1101 表示实际的 1 到 13,为啥呢,因为 0000 前面已经用过了,而 1110 跟 1111 也都有用了。
看个具体的例子(上下有点对不齐,将就看)
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
+|**zlbytes***| |***zltail***| |*zllen*| |entry1 entry2| |zlend|
+第一部分代表整个 ziplist 有 15 个字节,zlbytes 自己占了 4 个 zltail 表示最后一个元素的偏移量,第 13 个字节起,zllen 表示有 2 个元素,第一个元素是00f3,00表示前一个元素长度是 0,本来前面就没元素(不过不知道这个能不能优化这一字节),然后是 f3,换成二进制就是11110011,对照上面的注释,是落在|1111xxxx|这个类型里,注意这个其实是用 0001 到 1101 也就是 1到 13 来表示 0到 12,所以 f3 应该就是 2,第一个元素是 2,第二个元素呢,02 代表前一个元素也就是刚才说的这个,占用 2 字节,f6 展开也是刚才的类型,实际是 5,ff 表示 ziplist 的结尾,所以这个 ziplist 里面是两个元素,2 跟 5
说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
redis 针对这个策略的改进做了一个实验,这里借用下图
首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下
typedef struct redisObject {
- unsigned type:4;
- unsigned encoding:4;
- unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
- * LFU data (least significant 8 bits frequency
- * and most significant 16 bits access time). */
- int refcount;
- void *ptr;
-} robj;
-对于 lru 策略来说,lru 字段记录的就是redisObj 的LRU time,
redis 在访问数据时,都会调用lookupKey方法
/* Low level key lookup API, not actually called directly from commands
- * implementations that should instead rely on lookupKeyRead(),
- * lookupKeyWrite() and lookupKeyReadWithFlags(). */
-robj *lookupKey(redisDb *db, robj *key, int flags) {
- dictEntry *de = dictFind(db->dict,key->ptr);
- if (de) {
- robj *val = dictGetVal(de);
-
- /* Update the access time for the ageing algorithm.
- * Don't do it if we have a saving child, as this will trigger
- * a copy on write madness. */
- if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
- if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
- // 这个是后面一节的内容
- updateLFU(val);
- } else {
- // 对于这个分支,访问时就会去更新 lru 值
- val->lru = LRU_CLOCK();
- }
- }
- return val;
- } else {
- return NULL;
- }
-}
-/* This function is used to obtain the current LRU clock.
- * If the current resolution is lower than the frequency we refresh the
- * LRU clock (as it should be in production servers) we return the
- * precomputed value, otherwise we need to resort to a system call. */
-unsigned int LRU_CLOCK(void) {
- unsigned int lruclock;
- if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
- // 如果服务器的频率server.hz大于 1 时就是用系统预设的 lruclock
- lruclock = server.lruclock;
- } else {
- lruclock = getLRUClock();
- }
- return lruclock;
-}
-/* Return the LRU clock, based on the clock resolution. This is a time
- * in a reduced-bits format that can be used to set and check the
- * object->lru field of redisObject structures. */
-unsigned int getLRUClock(void) {
- return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
-}
-redis 处理命令是在这里processCommand
/* If this function gets called we already read a whole
- * command, arguments are in the client argv/argc fields.
- * processCommand() execute the command or prepare the
- * server for a bulk read from the client.
+ redis淘汰策略复习
+ /2021/08/01/redis%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5%E5%A4%8D%E4%B9%A0/
+ 前面复习了 redis 的过期策略,这里再复习下淘汰策略,淘汰跟过期的区别有时候会被混淆了,过期主要针对那些设置了过期时间的 key,应该说是一种逻辑策略,是主动的还是被动的加定时的,两种有各自的取舍,而淘汰也可以看成是一种保持系统稳定的策略,因为如果内存满了,不采取任何策略处理,那大概率会导致系统故障,之前其实主要从源码角度分析过redis 的 LRU 和 LFU,但这个是偏底层的实现,抠得比较细,那么具体的系统层面的配置是有哪些策略,来看下 redis labs 的介绍
+
+
+
+Policy
+Description
+
+
+
+noeviction 不逐出
+Returns an error if the memory limit has been reached when trying to insert more data,插入更多数据时,如果内存达到上限了,返回错误
+
+
+allkeys-lru 所有的 key 使用 lru 逐出
+Evicts the least recently used keys out of all keys 在所有 key 中逐出最近最少使用的
+
+
+allkeys-lfu 所有的 key 使用 lfu 逐出
+Evicts the least frequently used keys out of all keys 在所有 key 中逐出最近最不频繁使用的
+
+
+allkeys-random 所有的 key 中随机逐出
+Randomly evicts keys out of all keys 在所有 key 中随机逐出
+
+
+volatile-lru
+Evicts the least recently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lru 策略逐出
+
+
+volatile-lfu
+Evicts the least frequently used keys out of all keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中使用 lfu 策略逐出
+
+
+volatile-random
+Randomly evicts keys with an “expire” field set 在设置了过期时间的 key 空间 expire 中随机逐出
+
+
+volatile-ttl
+Evicts the shortest time-to-live keys out of all keys with an “expire” field set.在设置了过期时间的 key 空间 expire 中逐出更早过期的
+
+
+而在这其中默认使用的策略是 volatile-lru,对 lru 跟 lfu 想有更多的了解可以看下我之前的文章redis系列介绍八-淘汰策略
+]]>
+
+ redis
+
+
+ redis
+ 淘汰策略
+ 应用
+ Evict
+
+ redis 是如何过期缓存的,可以猜测下,最无脑的就是每个设置了过期时间的 key 都设个定时器,过期了就删除,这种显然消耗太大,清理地最及时,还有的就是 redis 正在采用的懒汉清理策略和定期清理
懒汉策略就是在使用的时候去检查缓存是否过期,比如 get 操作时,先判断下这个 key 是否已经过期了,如果过期了就删掉,并且返回空,如果没过期则正常返回
主要代码是
/* This function is called when we are going to perform some operation
+ * in a given key, but such key may be already logically expired even if
+ * it still exists in the database. The main way this function is called
+ * is via lookupKey*() family of functions.
*
- * If C_OK is returned the client is still alive and valid and
- * other operations can be performed by the caller. Otherwise
- * if C_ERR is returned the client was destroyed (i.e. after QUIT). */
-int processCommand(client *c) {
- moduleCallCommandFilters(c);
-
-
+ * The behavior of the function depends on the replication role of the
+ * instance, because slave instances do not expire keys, they wait
+ * for DELs from the master for consistency matters. However even
+ * slaves will try to have a coherent return value for the function,
+ * so that read commands executed in the slave side will be able to
+ * behave like if the key is expired even if still present (because the
+ * master has yet to propagate the DEL).
+ *
+ * In masters as a side effect of finding a key which is expired, such
+ * key will be evicted from the database. Also this may trigger the
+ * propagation of a DEL/UNLINK command in AOF / replication stream.
+ *
+ * The return value of the function is 0 if the key is still valid,
+ * otherwise the function returns 1 if the key is expired. */
+int expireIfNeeded(redisDb *db, robj *key) {
+ if (!keyIsExpired(db,key)) return 0;
- /* Handle the maxmemory directive.
+ /* If we are running in the context of a slave, instead of
+ * evicting the expired key from the database, we return ASAP:
+ * the slave key expiration is controlled by the master that will
+ * send us synthesized DEL operations for expired keys.
*
- * Note that we do not want to reclaim memory if we are here re-entering
- * the event loop since there is a busy Lua script running in timeout
- * condition, to avoid mixing the propagation of scripts with the
- * propagation of DELs due to eviction. */
- if (server.maxmemory && !server.lua_timedout) {
- int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
- /* freeMemoryIfNeeded may flush slave output buffers. This may result
- * into a slave, that may be the active client, to be freed. */
- if (server.current_client == NULL) return C_ERR;
+ * Still we try to return the right information to the caller,
+ * that is, 0 if we think the key should be still valid, 1 if
+ * we think the key is expired at this time. */
+ if (server.masterhost != NULL) return 1;
- /* It was impossible to free enough memory, and the command the client
- * is trying to execute is denied during OOM conditions or the client
- * is in MULTI/EXEC context? Error. */
- if (out_of_memory &&
- (c->cmd->flags & CMD_DENYOOM ||
- (c->flags & CLIENT_MULTI &&
- c->cmd->proc != execCommand &&
- c->cmd->proc != discardCommand)))
- {
- flagTransaction(c);
- addReply(c, shared.oomerr);
- return C_OK;
- }
- }
-}
-这里只摘了部分,当需要清理内存时就会调用, 然后调用了freeMemoryIfNeededAndSafe
/* This is a wrapper for freeMemoryIfNeeded() that only really calls the
- * function if right now there are the conditions to do so safely:
- *
- * - There must be no script in timeout condition.
- * - Nor we are loading data right now.
- *
- */
-int freeMemoryIfNeededAndSafe(void) {
- if (server.lua_timedout || server.loading) return C_OK;
- return freeMemoryIfNeeded();
+ /* Delete the key */
+ server.stat_expiredkeys++;
+ propagateExpire(db,key,server.lazyfree_lazy_expire);
+ notifyKeyspaceEvent(NOTIFY_EXPIRED,
+ "expired",key,db->id);
+ return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
+ dbSyncDelete(db,key);
}
-/* This function is periodically called to see if there is memory to free
- * according to the current "maxmemory" settings. In case we are over the
- * memory limit, the function will try to free some memory to return back
- * under the limit.
- *
- * The function returns C_OK if we are under the memory limit or if we
- * were over the limit, but the attempt to free memory was successful.
- * Otehrwise if we are over the memory limit, but not enough memory
- * was freed to return back under the limit, the function returns C_ERR. */
-int freeMemoryIfNeeded(void) {
- int keys_freed = 0;
- /* By default replicas should ignore maxmemory
- * and just be masters exact copies. */
- if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
- size_t mem_reported, mem_tofree, mem_freed;
- mstime_t latency, eviction_latency;
- long long delta;
- int slaves = listLength(server.slaves);
+/* Check if the key is expired. */
+int keyIsExpired(redisDb *db, robj *key) {
+ mstime_t when = getExpire(db,key);
+ mstime_t now;
- /* When clients are paused the dataset should be static not just from the
- * POV of clients not being able to write, but also from the POV of
- * expires and evictions of keys not being performed. */
- if (clientsArePaused()) return C_OK;
- if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
- return C_OK;
+ if (when < 0) return 0; /* No expire for this key */
- mem_freed = 0;
+ /* Don't expire anything while loading. It will be done later. */
+ if (server.loading) return 0;
- if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
- goto cant_free; /* We need to free memory, but policy forbids. */
+ /* If we are in the context of a Lua script, we pretend that time is
+ * blocked to when the Lua script started. This way a key can expire
+ * only the first time it is accessed and not in the middle of the
+ * script execution, making propagation to slaves / AOF consistent.
+ * See issue #1525 on Github for more information. */
+ if (server.lua_caller) {
+ now = server.lua_time_start;
+ }
+ /* If we are in the middle of a command execution, we still want to use
+ * a reference time that does not change: in that case we just use the
+ * cached time, that we update before each call in the call() function.
+ * This way we avoid that commands such as RPOPLPUSH or similar, that
+ * may re-open the same key multiple times, can invalidate an already
+ * open object in a next call, if the next call will see the key expired,
+ * while the first did not. */
+ else if (server.fixed_time_expire > 0) {
+ now = server.mstime;
+ }
+ /* For the other cases, we want to use the most fresh time we have. */
+ else {
+ now = mstime();
+ }
- latencyStartMonitor(latency);
- while (mem_freed < mem_tofree) {
- int j, k, i;
- static unsigned int next_db = 0;
- sds bestkey = NULL;
- int bestdbid;
- redisDb *db;
- dict *dict;
- dictEntry *de;
+ /* The key expired if the current (virtual or real) time is greater
+ * than the expire time of the key. */
+ return now > when;
+}
+/* Return the expire time of the specified key, or -1 if no expire
+ * is associated with this key (i.e. the key is non volatile) */
+long long getExpire(redisDb *db, robj *key) {
+ dictEntry *de;
- if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
- server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
- {
- struct evictionPoolEntry *pool = EvictionPoolLRU;
+ /* No expire? return ASAP */
+ if (dictSize(db->expires) == 0 ||
+ (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
- while(bestkey == NULL) {
- unsigned long total_keys = 0, keys;
+ /* The entry was found in the expire dict, this means it should also
+ * be present in the main dict (safety check). */
+ serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
+ return dictGetSignedIntegerVal(de);
+}
+这里有几点要注意的,第一是当惰性删除时会根据lazyfree_lazy_expire这个参数去判断是执行同步删除还是异步删除,另外一点是对于 slave,是不需要执行的,因为会在 master 过期时向 slave 发送 del 指令。
光采用这个策略会有什么问题呢,假如一些key 一直未被访问,那这些 key 就不会过期了,导致一直被占用着内存,所以 redis 采取了懒汉式过期加定期过期策略,定期策略是怎么执行的呢
/* This function handles 'background' operations we are required to do
+ * incrementally in Redis databases, such as active key expiring, resizing,
+ * rehashing. */
+void databasesCron(void) {
+ /* Expire keys by random sampling. Not required for slaves
+ * as master will synthesize DELs for us. */
+ if (server.active_expire_enabled) {
+ if (server.masterhost == NULL) {
+ activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
+ } else {
+ expireSlaveKeys();
+ }
+ }
- /* We don't want to make local-db choices when expiring keys,
- * so to start populate the eviction pool sampling keys from
- * every DB. */
- for (i = 0; i < server.dbnum; i++) {
- db = server.db+i;
- dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
- db->dict : db->expires;
- if ((keys = dictSize(dict)) != 0) {
- evictionPoolPopulate(i, dict, db->dict, pool);
- total_keys += keys;
- }
- }
- if (!total_keys) break; /* No keys to evict. */
+ /* Defrag keys gradually. */
+ activeDefragCycle();
- /* Go backward from best to worst element to evict. */
- for (k = EVPOOL_SIZE-1; k >= 0; k--) {
- if (pool[k].key == NULL) continue;
- bestdbid = pool[k].dbid;
+ /* Perform hash tables rehashing if needed, but only if there are no
+ * other processes saving the DB on disk. Otherwise rehashing is bad
+ * as will cause a lot of copy-on-write of memory pages. */
+ if (!hasActiveChildProcess()) {
+ /* We use global counters so if we stop the computation at a given
+ * DB we'll be able to start from the successive in the next
+ * cron loop iteration. */
+ static unsigned int resize_db = 0;
+ static unsigned int rehash_db = 0;
+ int dbs_per_call = CRON_DBS_PER_CALL;
+ int j;
- if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
- de = dictFind(server.db[pool[k].dbid].dict,
- pool[k].key);
- } else {
- de = dictFind(server.db[pool[k].dbid].expires,
- pool[k].key);
- }
+ /* Don't test more DBs than we have. */
+ if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
- /* Remove the entry from the pool. */
- if (pool[k].key != pool[k].cached)
- sdsfree(pool[k].key);
- pool[k].key = NULL;
- pool[k].idle = 0;
+ /* Resize */
+ for (j = 0; j < dbs_per_call; j++) {
+ tryResizeHashTables(resize_db % server.dbnum);
+ resize_db++;
+ }
- /* If the key exists, is our pick. Otherwise it is
- * a ghost and we need to try the next element. */
- if (de) {
- bestkey = dictGetKey(de);
- break;
- } else {
- /* Ghost... Iterate again. */
- }
- }
- }
- }
-
- /* volatile-random and allkeys-random policy */
- else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
- server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
- {
- /* When evicting a random key, we try to evict a key for
- * each DB, so we use the static 'next_db' variable to
- * incrementally visit all DBs. */
- for (i = 0; i < server.dbnum; i++) {
- j = (++next_db) % server.dbnum;
- db = server.db+j;
- dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
- db->dict : db->expires;
- if (dictSize(dict) != 0) {
- de = dictGetRandomKey(dict);
- bestkey = dictGetKey(de);
- bestdbid = j;
- break;
+ /* Rehash */
+ if (server.activerehashing) {
+ for (j = 0; j < dbs_per_call; j++) {
+ int work_done = incrementallyRehash(rehash_db);
+ if (work_done) {
+ /* If the function did some work, stop here, we'll do
+ * more at the next cron loop. */
+ break;
+ } else {
+ /* If this db didn't need rehash, we'll try the next one. */
+ rehash_db++;
+ rehash_db %= server.dbnum;
}
}
}
+ }
+}
+/* Try to expire a few timed out keys. The algorithm used is adaptive and
+ * will use few CPU cycles if there are few expiring keys, otherwise
+ * it will get more aggressive to avoid that too much memory is used by
+ * keys that can be removed from the keyspace.
+ *
+ * Every expire cycle tests multiple databases: the next call will start
+ * again from the next db, with the exception of exists for time limit: in that
+ * case we restart again from the last database we were processing. Anyway
+ * no more than CRON_DBS_PER_CALL databases are tested at every iteration.
+ *
+ * The function can perform more or less work, depending on the "type"
+ * argument. It can execute a "fast cycle" or a "slow cycle". The slow
+ * cycle is the main way we collect expired cycles: this happens with
+ * the "server.hz" frequency (usually 10 hertz).
+ *
+ * However the slow cycle can exit for timeout, since it used too much time.
+ * For this reason the function is also invoked to perform a fast cycle
+ * at every event loop cycle, in the beforeSleep() function. The fast cycle
+ * will try to perform less work, but will do it much more often.
+ *
+ * The following are the details of the two expire cycles and their stop
+ * conditions:
+ *
+ * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
+ * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
+ * microseconds, and is not repeated again before the same amount of time.
+ * The cycle will also refuse to run at all if the latest slow cycle did not
+ * terminate because of a time limit condition.
+ *
+ * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
+ * executed, where the time limit is a percentage of the REDIS_HZ period
+ * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
+ * fast cycle, the check of every database is interrupted once the number
+ * of already expired keys in the database is estimated to be lower than
+ * a given percentage, in order to avoid doing too much work to gain too
+ * little memory.
+ *
+ * The configured expire "effort" will modify the baseline parameters in
+ * order to do more work in both the fast and slow expire cycles.
+ */
- /* Finally remove the selected key. */
- if (bestkey) {
- db = server.db+bestdbid;
- robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
- propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
- /* We compute the amount of memory freed by db*Delete() alone.
- * It is possible that actually the memory needed to propagate
- * the DEL in AOF and replication link is greater than the one
- * we are freeing removing the key, but we can't account for
- * that otherwise we would never exit the loop.
- *
- * AOF and Output buffer memory will be freed eventually so
- * we only care about memory used by the key space. */
- delta = (long long) zmalloc_used_memory();
- latencyStartMonitor(eviction_latency);
- if (server.lazyfree_lazy_eviction)
- dbAsyncDelete(db,keyobj);
- else
- dbSyncDelete(db,keyobj);
- latencyEndMonitor(eviction_latency);
- latencyAddSampleIfNeeded("eviction-del",eviction_latency);
- latencyRemoveNestedEvent(latency,eviction_latency);
- delta -= (long long) zmalloc_used_memory();
- mem_freed += delta;
- server.stat_evictedkeys++;
- notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
- keyobj, db->id);
- decrRefCount(keyobj);
- keys_freed++;
+#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
+#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
+#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
+#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
+ we do extra efforts. */
+void activeExpireCycle(int type) {
+ /* Adjust the running parameters according to the configured expire
+ * effort. The default effort is 1, and the maximum configurable effort
+ * is 10. */
+ unsigned long
+ effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
+ config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
+ ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
+ config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
+ ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
+ config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
+ 2*effort,
+ config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
+ effort;
- /* When the memory to free starts to be big enough, we may
- * start spending so much time here that is impossible to
- * deliver data to the slaves fast enough, so we force the
- * transmission here inside the loop. */
- if (slaves) flushSlavesOutputBuffers();
+ /* This function has some global state in order to continue the work
+ * incrementally across calls. */
+ static unsigned int current_db = 0; /* Last DB tested. */
+ static int timelimit_exit = 0; /* Time limit hit in previous call? */
+ static long long last_fast_cycle = 0; /* When last fast cycle ran. */
- /* Normally our stop condition is the ability to release
- * a fixed, pre-computed amount of memory. However when we
- * are deleting objects in another thread, it's better to
- * check, from time to time, if we already reached our target
- * memory, since the "mem_freed" amount is computed only
- * across the dbAsyncDelete() call, while the thread can
- * release the memory all the time. */
- if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
- if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
- /* Let's satisfy our stop condition. */
- mem_freed = mem_tofree;
- }
- }
- } else {
- latencyEndMonitor(latency);
- latencyAddSampleIfNeeded("eviction-cycle",latency);
- goto cant_free; /* nothing to free... */
- }
- }
- latencyEndMonitor(latency);
- latencyAddSampleIfNeeded("eviction-cycle",latency);
- return C_OK;
+ int j, iteration = 0;
+ int dbs_per_call = CRON_DBS_PER_CALL;
+ long long start = ustime(), timelimit, elapsed;
-cant_free:
- /* We are here if we are not able to reclaim memory. There is only one
- * last thing we can try: check if the lazyfree thread has jobs in queue
- * and wait... */
- while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
- if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
- break;
- usleep(1000);
- }
- return C_ERR;
-}
-这里就是根据具体策略去淘汰 key,首先是要往 pool 更新 key,更新key 的方法是evictionPoolPopulate
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
- int j, k, count;
- dictEntry *samples[server.maxmemory_samples];
+ /* When clients are paused the dataset should be static not just from the
+ * POV of clients not being able to write, but also from the POV of
+ * expires and evictions of keys not being performed. */
+ if (clientsArePaused()) return;
- count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
- for (j = 0; j < count; j++) {
- unsigned long long idle;
- sds key;
- robj *o;
- dictEntry *de;
+ if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
+ /* Don't start a fast cycle if the previous cycle did not exit
+ * for time limit, unless the percentage of estimated stale keys is
+ * too high. Also never repeat a fast cycle for the same period
+ * as the fast cycle total duration itself. */
+ if (!timelimit_exit &&
+ server.stat_expired_stale_perc < config_cycle_acceptable_stale)
+ return;
- de = samples[j];
- key = dictGetKey(de);
+ if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
+ return;
- /* If the dictionary we are sampling from is not the main
- * dictionary (but the expires one) we need to lookup the key
- * again in the key dictionary to obtain the value object. */
- if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
- if (sampledict != keydict) de = dictFind(keydict, key);
- o = dictGetVal(de);
- }
+ last_fast_cycle = start;
+ }
- /* Calculate the idle time according to the policy. This is called
- * idle just because the code initially handled LRU, but is in fact
- * just a score where an higher score means better candidate. */
- if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
- idle = estimateObjectIdleTime(o);
- } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
- /* When we use an LRU policy, we sort the keys by idle time
- * so that we expire keys starting from greater idle time.
- * However when the policy is an LFU one, we have a frequency
- * estimation, and we want to evict keys with lower frequency
- * first. So inside the pool we put objects using the inverted
- * frequency subtracting the actual frequency to the maximum
- * frequency of 255. */
- idle = 255-LFUDecrAndReturn(o);
- } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
- /* In this case the sooner the expire the better. */
- idle = ULLONG_MAX - (long)dictGetVal(de);
- } else {
- serverPanic("Unknown eviction policy in evictionPoolPopulate()");
- }
+ /* We usually should test CRON_DBS_PER_CALL per iteration, with
+ * two exceptions:
+ *
+ * 1) Don't test more DBs than we have.
+ * 2) If last time we hit the time limit, we want to scan all DBs
+ * in this iteration, as there is work to do in some DB and we don't want
+ * expired keys to use memory for too much time. */
+ if (dbs_per_call > server.dbnum || timelimit_exit)
+ dbs_per_call = server.dbnum;
- /* Insert the element inside the pool.
- * First, find the first empty bucket or the first populated
- * bucket that has an idle time smaller than our idle time. */
- k = 0;
- while (k < EVPOOL_SIZE &&
- pool[k].key &&
- pool[k].idle < idle) k++;
- if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
- /* Can't insert if the element is < the worst element we have
- * and there are no empty buckets. */
- continue;
- } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
- /* Inserting into empty position. No setup needed before insert. */
- } else {
- /* Inserting in the middle. Now k points to the first element
- * greater than the element to insert. */
- if (pool[EVPOOL_SIZE-1].key == NULL) {
- /* Free space on the right? Insert at k shifting
- * all the elements from k to end to the right. */
+ /* We can use at max 'config_cycle_slow_time_perc' percentage of CPU
+ * time per iteration. Since this function gets called with a frequency of
+ * server.hz times per second, the following is the max amount of
+ * microseconds we can spend in this function. */
+ timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
+ timelimit_exit = 0;
+ if (timelimit <= 0) timelimit = 1;
- /* Save SDS before overwriting. */
- sds cached = pool[EVPOOL_SIZE-1].cached;
- memmove(pool+k+1,pool+k,
- sizeof(pool[0])*(EVPOOL_SIZE-k-1));
- pool[k].cached = cached;
- } else {
- /* No free space on right? Insert at k-1 */
- k--;
- /* Shift all elements on the left of k (included) to the
- * left, so we discard the element with smaller idle time. */
- sds cached = pool[0].cached; /* Save SDS before overwriting. */
- if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
- memmove(pool,pool+1,sizeof(pool[0])*k);
- pool[k].cached = cached;
- }
- }
+ if (type == ACTIVE_EXPIRE_CYCLE_FAST)
+ timelimit = config_cycle_fast_duration; /* in microseconds. */
- /* Try to reuse the cached SDS string allocated in the pool entry,
- * because allocating and deallocating this object is costly
- * (according to the profiler, not my fantasy. Remember:
- * premature optimizbla bla bla bla. */
- int klen = sdslen(key);
- if (klen > EVPOOL_CACHED_SDS_SIZE) {
- pool[k].key = sdsdup(key);
- } else {
- memcpy(pool[k].cached,key,klen+1);
- sdssetlen(pool[k].cached,klen);
- pool[k].key = pool[k].cached;
- }
- pool[k].idle = idle;
- pool[k].dbid = dbid;
- }
-}
-Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些键的空闲时间还大)就可以进pool。pool更新之后,就淘汰pool中空闲时间最大的键。
estimateObjectIdleTime用来计算Redis对象的空闲时间:
/* Given an object returns the min number of milliseconds the object was never
- * requested, using an approximated LRU algorithm. */
-unsigned long long estimateObjectIdleTime(robj *o) {
- unsigned long long lruclock = LRU_CLOCK();
- if (lruclock >= o->lru) {
- return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
- } else {
- return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
- LRU_CLOCK_RESOLUTION;
+ /* Accumulate some global stats as we expire keys, to have some idea
+ * about the number of keys that are already logically expired, but still
+ * existing inside the database. */
+ long total_sampled = 0;
+ long total_expired = 0;
+
+ for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
+ /* Expired and checked in a single loop. */
+ unsigned long expired, sampled;
+
+ redisDb *db = server.db+(current_db % server.dbnum);
+
+ /* Increment the DB now so we are sure if we run out of time
+ * in the current DB we'll restart from the next. This allows to
+ * distribute the time evenly across DBs. */
+ current_db++;
+
+ /* Continue to expire if at the end of the cycle more than 25%
+ * of the keys were expired. */
+ do {
+ unsigned long num, slots;
+ long long now, ttl_sum;
+ int ttl_samples;
+ iteration++;
+
+ /* If there is nothing to expire try next DB ASAP. */
+ if ((num = dictSize(db->expires)) == 0) {
+ db->avg_ttl = 0;
+ break;
+ }
+ slots = dictSlots(db->expires);
+ now = mstime();
+
+ /* When there are less than 1% filled slots, sampling the key
+ * space is expensive, so stop here waiting for better times...
+ * The dictionary will be resized asap. */
+ if (num && slots > DICT_HT_INITIAL_SIZE &&
+ (num*100/slots < 1)) break;
+
+ /* The main collection cycle. Sample random keys among keys
+ * with an expire set, checking for expired ones. */
+ expired = 0;
+ sampled = 0;
+ ttl_sum = 0;
+ ttl_samples = 0;
+
+ if (num > config_keys_per_loop)
+ num = config_keys_per_loop;
+
+ /* Here we access the low level representation of the hash table
+ * for speed concerns: this makes this code coupled with dict.c,
+ * but it hardly changed in ten years.
+ *
+ * Note that certain places of the hash table may be empty,
+ * so we want also a stop condition about the number of
+ * buckets that we scanned. However scanning for free buckets
+ * is very fast: we are in the cache line scanning a sequential
+ * array of NULL pointers, so we can scan a lot more buckets
+ * than keys in the same time. */
+ long max_buckets = num*20;
+ long checked_buckets = 0;
+
+ while (sampled < num && checked_buckets < max_buckets) {
+ for (int table = 0; table < 2; table++) {
+ if (table == 1 && !dictIsRehashing(db->expires)) break;
+
+ unsigned long idx = db->expires_cursor;
+ idx &= db->expires->ht[table].sizemask;
+ dictEntry *de = db->expires->ht[table].table[idx];
+ long long ttl;
+
+ /* Scan the current bucket of the current table. */
+ checked_buckets++;
+ while(de) {
+ /* Get the next entry now since this entry may get
+ * deleted. */
+ dictEntry *e = de;
+ de = de->next;
+
+ ttl = dictGetSignedIntegerVal(e)-now;
+ if (activeExpireCycleTryExpire(db,e,now)) expired++;
+ if (ttl > 0) {
+ /* We want the average TTL of keys yet
+ * not expired. */
+ ttl_sum += ttl;
+ ttl_samples++;
+ }
+ sampled++;
+ }
+ }
+ db->expires_cursor++;
+ }
+ total_expired += expired;
+ total_sampled += sampled;
+
+ /* Update the average TTL stats for this database. */
+ if (ttl_samples) {
+ long long avg_ttl = ttl_sum/ttl_samples;
+
+ /* Do a simple running average with a few samples.
+ * We just use the current estimate with a weight of 2%
+ * and the previous estimate with a weight of 98%. */
+ if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
+ db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
+ }
+
+ /* We can't block forever here even if there are many keys to
+ * expire. So after a given amount of milliseconds return to the
+ * caller waiting for the other active expire cycle. */
+ if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
+ elapsed = ustime()-start;
+ if (elapsed > timelimit) {
+ timelimit_exit = 1;
+ server.stat_expired_time_cap_reached_count++;
+ break;
+ }
+ }
+ /* We don't repeat the cycle for the current database if there are
+ * an acceptable amount of stale keys (logically expired but yet
+ * not reclained). */
+ } while ((expired*100/sampled) > config_cycle_acceptable_stale);
}
-}
-空闲时间第一种是 lurclock 大于对象的 lru,那么就是减一下乘以精度,因为 lruclock 有可能是已经预生成的,所以会可能走下面这个
-上面介绍了LRU 的算法,但是考虑一种场景
-~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
-~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
-~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
-~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
-可以发现,当采用 lru 的淘汰策略的时候,D 是最新的,会被认为是最值得保留的,但是事实上还不如 A 跟 B,然后 antirez 大神就想到了LFU (Least Frequently Used) 这个算法, 显然对于上面的四个 key 的访问频率,保留优先级应该是 B > A > C = D
那要怎么来实现这个 LFU 算法呢,其实像LRU,理想的情况就是维护个链表,把最新访问的放到头上去,但是这个会影响访问速度,注意到前面代码的应该可以看到,redisObject 的 lru 字段其实是两用的,当策略是 LFU 时,这个字段就另作他用了,它的 24 位长度被分成两部分
16 bits 8 bits
-+----------------+--------+
-+ Last decr time | LOG_C |
-+----------------+--------+
-前16位字段是最后一次递减时间,因此Redis知道 上一次计数器递减,后8位是 计数器 counter。
LFU 的主体策略就是当这个 key 被访问的次数越多频率越高他就越容易被保留下来,并且是最近被访问的频率越高。这其实有两个事情要做,一个是在访问的时候增加计数值,在一定长时间不访问时进行衰减,所以这里用了两个值,前 16 位记录上一次衰减的时间,后 8 位记录具体的计数值。
Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:
volatile-lfu:对有过期时间的key采用LFU淘汰策略allkeys-lfu:对全部key采用LFU淘汰策略
还有2个配置可以调整LFU算法:
lfu-log-factor 10
-lfu-decay-time 1
-```
-`lfu-log-factor` 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
-`lfu-decay-time`是一个以分钟为单位的数值,可以调整counter的减少速度
-这里有个问题是 8 位大小够计么,访问一次加 1 的话的确不够,不过大神就是大神,才不会这么简单的加一。往下看代码
+ elapsed = ustime()-start;
+ server.stat_expire_cycle_time_used += elapsed;
+ latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
+
+ /* Update our estimate of keys existing but yet to be expired.
+ * Running average with this sample accounting for 5%. */
+ double current_perc;
+ if (total_sampled) {
+ current_perc = (double)total_expired/total_sampled;
+ } else
+ current_perc = 0;
+ server.stat_expired_stale_perc = (current_perc*0.05)+
+ (server.stat_expired_stale_perc*0.95);
+}
+执行定期清除分成两种类型,快和慢,分别由beforeSleep和databasesCron调用,快版有两个限制,一个是执行时长由ACTIVE_EXPIRE_CYCLE_FAST_DURATION限制,另一个是执行间隔是 2 倍的ACTIVE_EXPIRE_CYCLE_FAST_DURATION,另外这还可以由配置的server.active_expire_effort参数来控制,默认是 1,最大是 10
onfig_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
+ ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort
+然后会从一定数量的 db 中找出一定数量的带过期时间的 key(保存在 expires中),这里的数量是由
+config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
+ ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort
+```
+控制,慢速的执行时长是
```C
-/* Low level key lookup API, not actually called directly from commands
+config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
+ 2*effort
+timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
+这里还有一个额外的退出条件,如果当前数据库的抽样结果已经达到我们所允许的过期 key 百分比,则下次不再处理当前 db,继续处理下个 db
+]]>说完了过期策略再说下淘汰策略,redis 使用的策略是近似的 lru 策略,为什么是近似的呢,先来看下什么是 lru,看下 wiki 的介绍
,图中一共有四个槽的存储空间,依次访问顺序是 A B C D E D F,
当第一次访问 D 时刚好占满了坑,并且值是 4,这个值越小代表越先被淘汰,当 E 进来时,看了下已经存在的四个里 A 是最小的,代表是最早存在并且最早被访问的,那就先淘汰它了,E 占领了 A 的位置,并设置值为 4,然后又访问 D 了,D 已经存在了,不过又被访问到了,得更新值为 5,然后是 F 进来了,这时 B 是最老的且最近未被访问,所以就淘汰它了。以上是一个 lru 的简要说明,但是 redis 没有严格按照这个去执行,理由跟前面过期策略一致,最严格的过期策略应该是每个 key 都有对应的定时器,当超时时马上就能清除,但是问题是这样的cpu 消耗太大,所换来的内存效率不太值得,淘汰策略也是这样,类似于上图,要维护所有 key 的一个有序 lru 值,并且遍历将最小的淘汰,redis 采用的是抽样的形式,最初的实现方式是随机从 dict 抽取 5 个 key,淘汰一个 lru 最小的,这样子勉强能达到淘汰的目的,但是效果不是特别好,后面在 redis 3.0开始,将随机抽取改成了维护一个 pool,pool 的大小默认是 16,每次放入的都是按lru 值有序排列好,每一次放入的必须是 lru小于 pool 中最小的 lru 才允许放入,直到放满,后面再有新的就会将大的踢出。
redis 针对这个策略的改进做了一个实验,这里借用下图
首先背景是这图中的所有点都对应一个 redis 的 key,灰色部分加入后被顺序访问过一遍,然后又加入了绿色部分,那么按照理论的 lru 算法,应该是图左上中,浅灰色部分全都被淘汰,那么对比来看看图右上,左下和右下,左下表示 2.8 版本就是随机抽样 5 个 key,淘汰其中 lru 最小的一个,发现是灰色和浅灰色的都有被淘汰的,右下的 3.0 版本抽样数量不变的情况下,稍好一些,当 3.0 版本的抽样数量调整成 10 后,已经较为接近理论上的 lru 策略了,通过代码来简要分析下
typedef struct redisObject {
+ unsigned type:4;
+ unsigned encoding:4;
+ unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
+ * LFU data (least significant 8 bits frequency
+ * and most significant 16 bits access time). */
+ int refcount;
+ void *ptr;
+} robj;
+对于 lru 策略来说,lru 字段记录的就是redisObj 的LRU time,
redis 在访问数据时,都会调用lookupKey方法
/* Low level key lookup API, not actually called directly from commands
* implementations that should instead rely on lookupKeyRead(),
* lookupKeyWrite() and lookupKeyReadWithFlags(). */
robj *lookupKey(redisDb *db, robj *key, int flags) {
@@ -10536,9 +10205,10 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
* a copy on write madness. */
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
- // 当淘汰策略是 LFU 时,就会调用这个updateLFU
+ // 这个是后面一节的内容
updateLFU(val);
} else {
+ // 对于这个分支,访问时就会去更新 lru 值
val->lru = LRU_CLOCK();
}
}
@@ -10546,1001 +10216,1265 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
} else {
return NULL;
}
-}
-updateLFU 这个其实个入口,调用了两个重要的方法
/* Update LFU when an object is accessed.
- * Firstly, decrement the counter if the decrement time is reached.
- * Then logarithmically increment the counter, and update the access time. */
-void updateLFU(robj *val) {
- unsigned long counter = LFUDecrAndReturn(val);
- counter = LFULogIncr(counter);
- val->lru = (LFUGetTimeInMinutes()<<8) | counter;
-}
-首先来看看LFUDecrAndReturn,这个方法的作用是根据上一次衰减时间和系统配置的 lfu-decay-time 参数来确定需要将 counter 减去多少
/* If the object decrement time is reached decrement the LFU counter but
- * do not update LFU fields of the object, we update the access time
- * and counter in an explicit way when the object is really accessed.
- * And we will times halve the counter according to the times of
- * elapsed time than server.lfu_decay_time.
- * Return the object frequency counter.
- *
- * This function is used in order to scan the dataset for the best object
- * to fit: as we check for the candidate, we incrementally decrement the
- * counter of the scanned objects if needed. */
-unsigned long LFUDecrAndReturn(robj *o) {
- // 右移 8 位,拿到上次衰减时间
- unsigned long ldt = o->lru >> 8;
- // 对 255 做与操作,拿到 counter 值
- unsigned long counter = o->lru & 255;
- // 根据lfu_decay_time来算出过了多少个衰减周期
- unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
- if (num_periods)
- counter = (num_periods > counter) ? 0 : counter - num_periods;
- return counter;
-}
-然后是加,调用了LFULogIncr
/* Logarithmically increment a counter. The greater is the current counter value
- * the less likely is that it gets really implemented. Saturate it at 255. */
-uint8_t LFULogIncr(uint8_t counter) {
- // 最大值就是 255,到顶了就不加了
- if (counter == 255) return 255;
- // 生成个随机小数
- double r = (double)rand()/RAND_MAX;
- // 减去个基础值,LFU_INIT_VAL = 5,防止刚进来就被逐出
- double baseval = counter - LFU_INIT_VAL;
- // 如果是小于 0,
- if (baseval < 0) baseval = 0;
- // 如果 baseval 是 0,那么 p 就是 1了,后面 counter 直接加一,如果不是的话,得看系统参数lfu_log_factor,这个越大,除出来的 p 越小,那么 counter++的可能性也越小,这样子就把前面的疑问给解决了,不是直接+1 的
- double p = 1.0/(baseval*server.lfu_log_factor+1);
- if (r < p) counter++;
- return counter;
-}
-大概的变化速度可以参考
-+--------+------------+------------+------------+------------+------------+
-| factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
-+--------+------------+------------+------------+------------+------------+
-| 0 | 104 | 255 | 255 | 255 | 255 |
-+--------+------------+------------+------------+------------+------------+
-| 1 | 18 | 49 | 255 | 255 | 255 |
-+--------+------------+------------+------------+------------+------------+
-| 10 | 10 | 18 | 142 | 255 | 255 |
-+--------+------------+------------+------------+------------+------------+
-| 100 | 8 | 11 | 49 | 143 | 255 |
-+--------+------------+------------+------------+------------+------------+
-简而言之就是 lfu_log_factor 越大变化的越慢
-总结一下,redis 实现了近似的 lru 淘汰策略,通过增加了淘汰 key 的池子(pool),并且增大每次抽样的 key 的数量来将淘汰效果更进一步地接近于 lru,这是 lru 策略,但是对于前面举的一个例子,其实 lru 并不能保证 key 的淘汰就如我们预期,所以在后期又引入了 lfu 的策略,lfu的策略比较巧妙,复用了 redis 对象的 lru 字段,并且使用了factor 参数来控制计数器递增的速度,防止 8 位的计数器太早溢出。
-]]>之前其实写过redis的过期的一些原理,这次主要是记录下,一些使用上的概念,主要是redis使用的过期策略是懒过期和定时清除,懒过期的其实比较简单,即是在key被访问的时候会顺带着判断下这个key是否已过期了,如果已经过期了,就不返回了,但是这种策略有个漏洞是如果有些key之后一直不会被访问了,就等于沉在池底了,所以需要有一个定时的清理机制,去从设置了过期的key池子(expires)里随机地捞key,具体的策略我们看下官网的解释
-从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件
-# Redis calls an internal function to perform many background tasks, like
-# closing connections of clients in timeout, purging expired keys that are
-# never requested, and so forth.
-#
-# Not all tasks are performed with the same frequency, but Redis checks for
-# tasks to perform according to the specified "hz" value.
-#
-# By default "hz" is set to 10. Raising the value will use more CPU when
-# Redis is idle, but at the same time will make Redis more responsive when
-# there are many keys expiring at the same time, and timeouts may be
-# handled with more precision.
-#
-# The range is between 1 and 500, however a value over 100 is usually not
-# a good idea. Most users should use the default of 10 and raise this up to
-# 100 only in environments where very low latency is required.
-hz 10
+}
+/* This function is used to obtain the current LRU clock.
+ * If the current resolution is lower than the frequency we refresh the
+ * LRU clock (as it should be in production servers) we return the
+ * precomputed value, otherwise we need to resort to a system call. */
+unsigned int LRU_CLOCK(void) {
+ unsigned int lruclock;
+ if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
+ // 如果服务器的频率server.hz大于 1 时就是用系统预设的 lruclock
+ lruclock = server.lruclock;
+ } else {
+ lruclock = getLRUClock();
+ }
+ return lruclock;
+}
+/* Return the LRU clock, based on the clock resolution. This is a time
+ * in a reduced-bits format that can be used to set and check the
+ * object->lru field of redisObject structures. */
+unsigned int getLRUClock(void) {
+ return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
+}
+redis 处理命令是在这里processCommand
/* If this function gets called we already read a whole
+ * command, arguments are in the client argv/argc fields.
+ * processCommand() execute the command or prepare the
+ * server for a bulk read from the client.
+ *
+ * If C_OK is returned the client is still alive and valid and
+ * other operations can be performed by the caller. Otherwise
+ * if C_ERR is returned the client was destroyed (i.e. after QUIT). */
+int processCommand(client *c) {
+ moduleCallCommandFilters(c);
-可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略
-]]>fn main() {
- let mut s = String::from("hello world");
+
- let word = first_word(&s);
+ /* Handle the maxmemory directive.
+ *
+ * Note that we do not want to reclaim memory if we are here re-entering
+ * the event loop since there is a busy Lua script running in timeout
+ * condition, to avoid mixing the propagation of scripts with the
+ * propagation of DELs due to eviction. */
+ if (server.maxmemory && !server.lua_timedout) {
+ int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
+ /* freeMemoryIfNeeded may flush slave output buffers. This may result
+ * into a slave, that may be the active client, to be freed. */
+ if (server.current_client == NULL) return C_ERR;
- s.clear();
+ /* It was impossible to free enough memory, and the command the client
+ * is trying to execute is denied during OOM conditions or the client
+ * is in MULTI/EXEC context? Error. */
+ if (out_of_memory &&
+ (c->cmd->flags & CMD_DENYOOM ||
+ (c->flags & CLIENT_MULTI &&
+ c->cmd->proc != execCommand &&
+ c->cmd->proc != discardCommand)))
+ {
+ flagTransaction(c);
+ addReply(c, shared.oomerr);
+ return C_OK;
+ }
+ }
+}
+这里只摘了部分,当需要清理内存时就会调用, 然后调用了freeMemoryIfNeededAndSafe
/* This is a wrapper for freeMemoryIfNeeded() that only really calls the
+ * function if right now there are the conditions to do so safely:
+ *
+ * - There must be no script in timeout condition.
+ * - Nor we are loading data right now.
+ *
+ */
+int freeMemoryIfNeededAndSafe(void) {
+ if (server.lua_timedout || server.loading) return C_OK;
+ return freeMemoryIfNeeded();
+}
+/* This function is periodically called to see if there is memory to free
+ * according to the current "maxmemory" settings. In case we are over the
+ * memory limit, the function will try to free some memory to return back
+ * under the limit.
+ *
+ * The function returns C_OK if we are under the memory limit or if we
+ * were over the limit, but the attempt to free memory was successful.
+ * Otehrwise if we are over the memory limit, but not enough memory
+ * was freed to return back under the limit, the function returns C_ERR. */
+int freeMemoryIfNeeded(void) {
+ int keys_freed = 0;
+ /* By default replicas should ignore maxmemory
+ * and just be masters exact copies. */
+ if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
- // 这时候虽然 word 还是 5,但是 s 已经被清除了,所以就没存在的意义
-}
-这里其实我们就需要关注 s 的存在性,代码的逻辑合理性就需要额外去维护,此时我们就可以用切片
-let s = String::from("hello world")
+ size_t mem_reported, mem_tofree, mem_freed;
+ mstime_t latency, eviction_latency;
+ long long delta;
+ int slaves = listLength(server.slaves);
-let hello = &s[0..5];
-let world = &s[6..11];
-其实跟 Python 的list 之类的语法有点类似,当然里面还有些语法糖,比如可以直接用省略后面的数字表示直接引用到结尾
-let hello = &s[0..];
-甚至再进一步
-let hello = &s[..];
-使用了切片之后
-fn first_word(s: &String) -> &str {
- let bytes = s.as_bytes();
+ /* When clients are paused the dataset should be static not just from the
+ * POV of clients not being able to write, but also from the POV of
+ * expires and evictions of keys not being performed. */
+ if (clientsArePaused()) return C_OK;
+ if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
+ return C_OK;
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return &s[0..i];
- }
- }
+ mem_freed = 0;
- &s[..]
-}
-fn main() {
- let mut s = String::from("hello world");
+ if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
+ goto cant_free; /* We need to free memory, but policy forbids. */
- let word = first_word(&s);
+ latencyStartMonitor(latency);
+ while (mem_freed < mem_tofree) {
+ int j, k, i;
+ static unsigned int next_db = 0;
+ sds bestkey = NULL;
+ int bestdbid;
+ redisDb *db;
+ dict *dict;
+ dictEntry *de;
- s.clear(); // error!
+ if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
+ server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
+ {
+ struct evictionPoolEntry *pool = EvictionPoolLRU;
- println!("the first word is: {}", word);
-}
-那再执行 main 函数的时候就会抛错,因为 word 还是个切片,需要保证 s 的有效性,并且其实我们可以将函数申明成
-fn first_word(s: &str) -> &str {
-这样就既能处理&String 的情况,就是当成完整字符串的切片,也能处理普通的切片。
其他类型的切片
let a = [1, 2, 3, 4, 5];
-let slice = &a[1..3];
-简单记录下,具体可以去看看这本书
-]]>然后看个案例
-let x = 5;
-let y = x;
-这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
但是对于非基础类型
let s1 = String::from("hello");
-let s2 = s1;
+ while(bestkey == NULL) {
+ unsigned long total_keys = 0, keys;
-println!("{}, world!", s1);
-有可能认为有两种内存分布可能
先看下 string 的内存结构
第一种可能是
第二种是
我们来尝试编译下
发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了
这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法
let s1 = String::from("hello");
-let s2 = s1.clone();
+ /* We don't want to make local-db choices when expiring keys,
+ * so to start populate the eviction pool sampling keys from
+ * every DB. */
+ for (i = 0; i < server.dbnum; i++) {
+ db = server.db+i;
+ dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
+ db->dict : db->expires;
+ if ((keys = dictSize(dict)) != 0) {
+ evictionPoolPopulate(i, dict, db->dict, pool);
+ total_keys += keys;
+ }
+ }
+ if (!total_keys) break; /* No keys to evict. */
-println!("s1 = {}, s2 = {}", s1, s2);
-这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配
$nums=2,0,1,2
-顺便可以用下我们上次学到的gettype()
如果是想创建连续数字的数组还可以用这个方便的方法
-$nums=1..5
-
而且数组还可以存放各种类型的数据
$array=1,"哈哈",([System.Guid]::NewGuid()),(get-date)
-
还有判断类型可以用-is
创建一个空数组
$array=@()
-
数组添加元素
$array+="a"
-
数组删除元素
$a=1..4
-$a=$a[0..1]+$a[3]
-
-public class CustomSpringEvent extends ApplicationEvent {
+ /* Go backward from best to worst element to evict. */
+ for (k = EVPOOL_SIZE-1; k >= 0; k--) {
+ if (pool[k].key == NULL) continue;
+ bestdbid = pool[k].dbid;
- private String message;
+ if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
+ de = dictFind(server.db[pool[k].dbid].dict,
+ pool[k].key);
+ } else {
+ de = dictFind(server.db[pool[k].dbid].expires,
+ pool[k].key);
+ }
- public CustomSpringEvent(Object source, String message) {
- super(source);
- this.message = message;
- }
- public String getMessage() {
- return message;
- }
-}
-这个 ApplicationEvent 其实也比较简单,内部就一个 Object 类型的 source,可以自行扩展,我们在自定义的这个 Event 里加了个 Message ,只是简单介绍下使用
-public abstract class ApplicationEvent extends EventObject {
- private static final long serialVersionUID = 7099057708183571937L;
- private final long timestamp;
+ /* Remove the entry from the pool. */
+ if (pool[k].key != pool[k].cached)
+ sdsfree(pool[k].key);
+ pool[k].key = NULL;
+ pool[k].idle = 0;
- public ApplicationEvent(Object source) {
- super(source);
- this.timestamp = System.currentTimeMillis();
- }
+ /* If the key exists, is our pick. Otherwise it is
+ * a ghost and we need to try the next element. */
+ if (de) {
+ bestkey = dictGetKey(de);
+ break;
+ } else {
+ /* Ghost... Iterate again. */
+ }
+ }
+ }
+ }
- public ApplicationEvent(Object source, Clock clock) {
- super(source);
- this.timestamp = clock.millis();
- }
+ /* volatile-random and allkeys-random policy */
+ else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
+ server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
+ {
+ /* When evicting a random key, we try to evict a key for
+ * each DB, so we use the static 'next_db' variable to
+ * incrementally visit all DBs. */
+ for (i = 0; i < server.dbnum; i++) {
+ j = (++next_db) % server.dbnum;
+ db = server.db+j;
+ dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
+ db->dict : db->expires;
+ if (dictSize(dict) != 0) {
+ de = dictGetRandomKey(dict);
+ bestkey = dictGetKey(de);
+ bestdbid = j;
+ break;
+ }
+ }
+ }
- public final long getTimestamp() {
- return this.timestamp;
- }
-}
+ /* Finally remove the selected key. */
+ if (bestkey) {
+ db = server.db+bestdbid;
+ robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
+ propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
+ /* We compute the amount of memory freed by db*Delete() alone.
+ * It is possible that actually the memory needed to propagate
+ * the DEL in AOF and replication link is greater than the one
+ * we are freeing removing the key, but we can't account for
+ * that otherwise we would never exit the loop.
+ *
+ * AOF and Output buffer memory will be freed eventually so
+ * we only care about memory used by the key space. */
+ delta = (long long) zmalloc_used_memory();
+ latencyStartMonitor(eviction_latency);
+ if (server.lazyfree_lazy_eviction)
+ dbAsyncDelete(db,keyobj);
+ else
+ dbSyncDelete(db,keyobj);
+ latencyEndMonitor(eviction_latency);
+ latencyAddSampleIfNeeded("eviction-del",eviction_latency);
+ latencyRemoveNestedEvent(latency,eviction_latency);
+ delta -= (long long) zmalloc_used_memory();
+ mem_freed += delta;
+ server.stat_evictedkeys++;
+ notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
+ keyobj, db->id);
+ decrRefCount(keyobj);
+ keys_freed++;
-然后就是事件生产者和监听消费者
-@Component
-public class CustomSpringEventPublisher {
+ /* When the memory to free starts to be big enough, we may
+ * start spending so much time here that is impossible to
+ * deliver data to the slaves fast enough, so we force the
+ * transmission here inside the loop. */
+ if (slaves) flushSlavesOutputBuffers();
- @Resource
- private ApplicationEventPublisher applicationEventPublisher;
+ /* Normally our stop condition is the ability to release
+ * a fixed, pre-computed amount of memory. However when we
+ * are deleting objects in another thread, it's better to
+ * check, from time to time, if we already reached our target
+ * memory, since the "mem_freed" amount is computed only
+ * across the dbAsyncDelete() call, while the thread can
+ * release the memory all the time. */
+ if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
+ if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
+ /* Let's satisfy our stop condition. */
+ mem_freed = mem_tofree;
+ }
+ }
+ } else {
+ latencyEndMonitor(latency);
+ latencyAddSampleIfNeeded("eviction-cycle",latency);
+ goto cant_free; /* nothing to free... */
+ }
+ }
+ latencyEndMonitor(latency);
+ latencyAddSampleIfNeeded("eviction-cycle",latency);
+ return C_OK;
- public void publishCustomEvent(final String message) {
- System.out.println("Publishing custom event. ");
- CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
- applicationEventPublisher.publishEvent(customSpringEvent);
- }
-}
-这里的 ApplicationEventPublisher 是 Spring 的方法接口
-@FunctionalInterface
-public interface ApplicationEventPublisher {
- default void publishEvent(ApplicationEvent event) {
- this.publishEvent((Object)event);
- }
+cant_free:
+ /* We are here if we are not able to reclaim memory. There is only one
+ * last thing we can try: check if the lazyfree thread has jobs in queue
+ * and wait... */
+ while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
+ if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
+ break;
+ usleep(1000);
+ }
+ return C_ERR;
+}
+这里就是根据具体策略去淘汰 key,首先是要往 pool 更新 key,更新key 的方法是evictionPoolPopulate
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
+ int j, k, count;
+ dictEntry *samples[server.maxmemory_samples];
- void publishEvent(Object var1);
-}
-具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲
事件监听者:
-@Component
-public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
- @Override
- public void onApplicationEvent(CustomSpringEvent event) {
- System.out.println("Received spring custom event - " + event.getMessage());
- }
-}
-这里的也是 spring 的一个方法接口
-@FunctionalInterface
-public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
- void onApplicationEvent(E var1);
+ count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
+ for (j = 0; j < count; j++) {
+ unsigned long long idle;
+ sds key;
+ robj *o;
+ dictEntry *de;
- static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
- return (event) -> {
- consumer.accept(event.getPayload());
- };
- }
-}
+ de = samples[j];
+ key = dictGetKey(de);
-然后简单包个请求
-
-@RequestMapping(value = "/event", method = RequestMethod.GET)
-@ResponseBody
-public void event() {
- customSpringEventPublisher.publishCustomEvent("hello sprint event");
-}
+ /* If the dictionary we are sampling from is not the main
+ * dictionary (but the expires one) we need to lookup the key
+ * again in the key dictionary to obtain the value object. */
+ if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
+ if (sampledict != keydict) de = dictFind(keydict, key);
+ o = dictGetVal(de);
+ }
-
就能看到接收到消息了。
bind: Cannot assign requested address
-查了下这个问题,猜测是不是端口已经被占用了,查了下并不是,然后想到是不是端口是系统保留的,
-sysctl -a |grep port_range
-结果中
-net.ipv4.ip_local_port_range = 50000 65000 -----意味着50000~65000端口可用
-发现也不是,没有限制,最后才查到这个原因是默认如果有 ipv6 的话会使用 ipv6 的地址做映射
所以如果是命令连接做端口转发的话,
ssh -4 -L 11234:localhost:1234 x.x.x.x
-使用-4来制定通过 ipv4 地址来做映射
如果是在 .ssh/config 中配置的话可以直接指定所有的连接都走 ipv4
Host *
- AddressFamily inet
-inet代表 ipv4,inet6代表 ipv6
AddressFamily 的所有取值范围是:”any”(默认)、”inet”(仅IPv4)、”inet6”(仅IPv6)。
另外此类问题还可以通过 ssh -v 来打印更具体的信息
Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些键的空闲时间还大)就可以进pool。pool更新之后,就淘汰pool中空闲时间最大的键。
estimateObjectIdleTime用来计算Redis对象的空闲时间:
/* Given an object returns the min number of milliseconds the object was never
+ * requested, using an approximated LRU algorithm. */
+unsigned long long estimateObjectIdleTime(robj *o) {
+ unsigned long long lruclock = LRU_CLOCK();
+ if (lruclock >= o->lru) {
+ return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
+ } else {
+ return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
+ LRU_CLOCK_RESOLUTION;
+ }
+}
+空闲时间第一种是 lurclock 大于对象的 lru,那么就是减一下乘以精度,因为 lruclock 有可能是已经预生成的,所以会可能走下面这个
+上面介绍了LRU 的算法,但是考虑一种场景
+~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
+~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
+~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
+~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
+可以发现,当采用 lru 的淘汰策略的时候,D 是最新的,会被认为是最值得保留的,但是事实上还不如 A 跟 B,然后 antirez 大神就想到了LFU (Least Frequently Used) 这个算法, 显然对于上面的四个 key 的访问频率,保留优先级应该是 B > A > C = D
那要怎么来实现这个 LFU 算法呢,其实像LRU,理想的情况就是维护个链表,把最新访问的放到头上去,但是这个会影响访问速度,注意到前面代码的应该可以看到,redisObject 的 lru 字段其实是两用的,当策略是 LFU 时,这个字段就另作他用了,它的 24 位长度被分成两部分
16 bits 8 bits
++----------------+--------+
++ Last decr time | LOG_C |
++----------------+--------+
+前16位字段是最后一次递减时间,因此Redis知道 上一次计数器递减,后8位是 计数器 counter。
LFU 的主体策略就是当这个 key 被访问的次数越多频率越高他就越容易被保留下来,并且是最近被访问的频率越高。这其实有两个事情要做,一个是在访问的时候增加计数值,在一定长时间不访问时进行衰减,所以这里用了两个值,前 16 位记录上一次衰减的时间,后 8 位记录具体的计数值。
Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:
volatile-lfu:对有过期时间的key采用LFU淘汰策略allkeys-lfu:对全部key采用LFU淘汰策略
还有2个配置可以调整LFU算法:
lfu-log-factor 10
+lfu-decay-time 1
+```
+`lfu-log-factor` 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
+
+`lfu-decay-time`是一个以分钟为单位的数值,可以调整counter的减少速度
+这里有个问题是 8 位大小够计么,访问一次加 1 的话的确不够,不过大神就是大神,才不会这么简单的加一。往下看代码
+```C
+/* Low level key lookup API, not actually called directly from commands
+ * implementations that should instead rely on lookupKeyRead(),
+ * lookupKeyWrite() and lookupKeyReadWithFlags(). */
+robj *lookupKey(redisDb *db, robj *key, int flags) {
+ dictEntry *de = dictFind(db->dict,key->ptr);
+ if (de) {
+ robj *val = dictGetVal(de);
+
+ /* Update the access time for the ageing algorithm.
+ * Don't do it if we have a saving child, as this will trigger
+ * a copy on write madness. */
+ if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
+ if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
+ // 当淘汰策略是 LFU 时,就会调用这个updateLFU
+ updateLFU(val);
+ } else {
+ val->lru = LRU_CLOCK();
+ }
+ }
+ return val;
+ } else {
+ return NULL;
+ }
+}
+updateLFU 这个其实个入口,调用了两个重要的方法
/* Update LFU when an object is accessed.
+ * Firstly, decrement the counter if the decrement time is reached.
+ * Then logarithmically increment the counter, and update the access time. */
+void updateLFU(robj *val) {
+ unsigned long counter = LFUDecrAndReturn(val);
+ counter = LFULogIncr(counter);
+ val->lru = (LFUGetTimeInMinutes()<<8) | counter;
+}
+首先来看看LFUDecrAndReturn,这个方法的作用是根据上一次衰减时间和系统配置的 lfu-decay-time 参数来确定需要将 counter 减去多少
/* If the object decrement time is reached decrement the LFU counter but
+ * do not update LFU fields of the object, we update the access time
+ * and counter in an explicit way when the object is really accessed.
+ * And we will times halve the counter according to the times of
+ * elapsed time than server.lfu_decay_time.
+ * Return the object frequency counter.
+ *
+ * This function is used in order to scan the dataset for the best object
+ * to fit: as we check for the candidate, we incrementally decrement the
+ * counter of the scanned objects if needed. */
+unsigned long LFUDecrAndReturn(robj *o) {
+ // 右移 8 位,拿到上次衰减时间
+ unsigned long ldt = o->lru >> 8;
+ // 对 255 做与操作,拿到 counter 值
+ unsigned long counter = o->lru & 255;
+ // 根据lfu_decay_time来算出过了多少个衰减周期
+ unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
+ if (num_periods)
+ counter = (num_periods > counter) ? 0 : counter - num_periods;
+ return counter;
+}
+然后是加,调用了LFULogIncr
/* Logarithmically increment a counter. The greater is the current counter value
+ * the less likely is that it gets really implemented. Saturate it at 255. */
+uint8_t LFULogIncr(uint8_t counter) {
+ // 最大值就是 255,到顶了就不加了
+ if (counter == 255) return 255;
+ // 生成个随机小数
+ double r = (double)rand()/RAND_MAX;
+ // 减去个基础值,LFU_INIT_VAL = 5,防止刚进来就被逐出
+ double baseval = counter - LFU_INIT_VAL;
+ // 如果是小于 0,
+ if (baseval < 0) baseval = 0;
+ // 如果 baseval 是 0,那么 p 就是 1了,后面 counter 直接加一,如果不是的话,得看系统参数lfu_log_factor,这个越大,除出来的 p 越小,那么 counter++的可能性也越小,这样子就把前面的疑问给解决了,不是直接+1 的
+ double p = 1.0/(baseval*server.lfu_log_factor+1);
+ if (r < p) counter++;
+ return counter;
+}
+大概的变化速度可以参考
++--------+------------+------------+------------+------------+------------+
+| factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
++--------+------------+------------+------------+------------+------------+
+| 0 | 104 | 255 | 255 | 255 | 255 |
++--------+------------+------------+------------+------------+------------+
+| 1 | 18 | 49 | 255 | 255 | 255 |
++--------+------------+------------+------------+------------+------------+
+| 10 | 10 | 18 | 142 | 255 | 255 |
++--------+------------+------------+------------+------------+------------+
+| 100 | 8 | 11 | 49 | 143 | 255 |
++--------+------------+------------+------------+------------+------------+
+简而言之就是 lfu_log_factor 越大变化的越慢
+总结一下,redis 实现了近似的 lru 淘汰策略,通过增加了淘汰 key 的池子(pool),并且增大每次抽样的 key 的数量来将淘汰效果更进一步地接近于 lru,这是 lru 策略,但是对于前面举的一个例子,其实 lru 并不能保证 key 的淘汰就如我们预期,所以在后期又引入了 lfu 的策略,lfu的策略比较巧妙,复用了 redis 对象的 lru 字段,并且使用了factor 参数来控制计数器递增的速度,防止 8 位的计数器太早溢出。
]]>Given a sorted integer array without duplicates, return the summary of its ranges.
-For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].
每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考
class Solution {
-public:
- vector<string> summaryRanges(vector<int>& nums) {
- int i = 0, j = 1, n;
- vector<string> res;
- n = nums.size();
- while(i < n){
- j = 1;
- while(j < n && nums[i+j] - nums[i] == j) j++;
- res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
- i += j;
- }
- return res;
- }
-};]]>之前其实写过redis的过期的一些原理,这次主要是记录下,一些使用上的概念,主要是redis使用的过期策略是懒过期和定时清除,懒过期的其实比较简单,即是在key被访问的时候会顺带着判断下这个key是否已过期了,如果已经过期了,就不返回了,但是这种策略有个漏洞是如果有些key之后一直不会被访问了,就等于沉在池底了,所以需要有一个定时的清理机制,去从设置了过期的key池子(expires)里随机地捞key,具体的策略我们看下官网的解释
+从池子里随机获取20个key,将其中过期的key删掉,如果这其中有超过25%的key已经过期了,那就再来一次,以此保持过期的key不超过25%(左右),并且这个定时策略可以在redis的配置文件
+# Redis calls an internal function to perform many background tasks, like
+# closing connections of clients in timeout, purging expired keys that are
+# never requested, and so forth.
+#
+# Not all tasks are performed with the same frequency, but Redis checks for
+# tasks to perform according to the specified "hz" value.
+#
+# By default "hz" is set to 10. Raising the value will use more CPU when
+# Redis is idle, but at the same time will make Redis more responsive when
+# there are many keys expiring at the same time, and timeouts may be
+# handled with more precision.
+#
+# The range is between 1 and 500, however a value over 100 is usually not
+# a good idea. Most users should use the default of 10 and raise this up to
+# 100 only in environments where very low latency is required.
+hz 10
+
+可以配置这个hz的值,代表的含义是每秒的执行次数,默认是10,其实也用了hz的普遍含义。有兴趣可以看看之前写的一篇文章redis系列介绍七-过期策略
+]]>WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。
,在web私信,im等应用较多。背景和优缺点可以参看wiki。
因为swoole官方还不支持windows,所以需要装下linux,之前都是用ubuntu,
这次就试一下centos7,还是满好看的,虽然虚拟机会默认最小安装,需要在安装
时自己选择带gnome的,当然最小安装也是可以的,只是最后需要改下防火墙。
然后是装下PHP,Nginx什么的,我是用Oneinstack,可以按需安装
给做这个的大大点个赞。
1.install via pecl
-pecl install swoole
+ $nums=2,0,1,2
+顺便可以用下我们上次学到的gettype()
如果是想创建连续数字的数组还可以用这个方便的方法
+$nums=1..5
+
而且数组还可以存放各种类型的数据
$array=1,"哈哈",([System.Guid]::NewGuid()),(get-date)
+
还有判断类型可以用-is
创建一个空数组
$array=@()
+
数组添加元素
$array+="a"
+
数组删除元素
$a=1..4
+$a=$a[0..1]+$a[3]
+
fn main() {
+ let mut s = String::from("hello world");
-2.install from source
-sudo apt-get install php5-dev
-git clone https://github.com/swoole/swoole-src.git
-cd swoole-src
-phpize
-./configure
-make && make install
-3.add extension
-extension = swoole.so
+ let word = first_word(&s);
-4.test extension
-php -m | grep swoole
-如果存在就代表安装成功啦
-Exec
实现代码看了这位仁兄的代码
-还是贴一下代码
服务端:
-//创建websocket服务器对象,监听0.0.0.0:9502端口
-$ws = new swoole_websocket_server("0.0.0.0", 9502);
+ s.clear();
-//监听WebSocket连接打开事件
-$ws->on('open', function ($ws, $request) {
- $fd[] = $request->fd;
- $GLOBALS['fd'][] = $fd;
- //区别下当前用户
- $ws->push($request->fd, "hello user{$request->fd}, welcome\n");
-});
+ // 这时候虽然 word 还是 5,但是 s 已经被清除了,所以就没存在的意义
+}
+这里其实我们就需要关注 s 的存在性,代码的逻辑合理性就需要额外去维护,此时我们就可以用切片
+let s = String::from("hello world")
-//监听WebSocket消息事件
-$ws->on('message', function ($ws, $frame) {
- $msg = 'from'.$frame->fd.":{$frame->data}\n";
+let hello = &s[0..5];
+let world = &s[6..11];
+其实跟 Python 的list 之类的语法有点类似,当然里面还有些语法糖,比如可以直接用省略后面的数字表示直接引用到结尾
+let hello = &s[0..];
+甚至再进一步
+let hello = &s[..];
+使用了切片之后
+fn first_word(s: &String) -> &str {
+ let bytes = s.as_bytes();
- foreach($GLOBALS['fd'] as $aa){
- foreach($aa as $i){
- if($i != $frame->fd) {
- # code...
- $ws->push($i,$msg);
- }
+ for (i, &item) in bytes.iter().enumerate() {
+ if item == b' ' {
+ return &s[0..i];
}
}
-});
-
-//监听WebSocket连接关闭事件
-$ws->on('close', function ($ws, $fd) {
- echo "client-{$fd} is closed\n";
-});
-
-$ws->start();
-
-客户端:
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <title>Title</title>
-</head>
-<body>
-<div id="msg"></div>
-<input type="text" id="text">
-<input type="submit" value="发送数据" onclick="song()">
-</body>
-<script>
- var msg = document.getElementById("msg");
- var wsServer = 'ws://0.0.0.0:9502';
- //调用websocket对象建立连接:
- //参数:ws/wss(加密)://ip:port (字符串)
- var websocket = new WebSocket(wsServer);
- //onopen监听连接打开
- websocket.onopen = function (evt) {
- //websocket.readyState 属性:
- /*
- CONNECTING 0 The connection is not yet open.
- OPEN 1 The connection is open and ready to communicate.
- CLOSING 2 The connection is in the process of closing.
- CLOSED 3 The connection is closed or couldn't be opened.
- */
- msg.innerHTML = websocket.readyState;
- };
- function song(){
- var text = document.getElementById('text').value;
- document.getElementById('text').value = '';
- //向服务器发送数据
- websocket.send(text);
- }
- //监听连接关闭
-// websocket.onclose = function (evt) {
-// console.log("Disconnected");
-// };
+ &s[..]
+}
+fn main() {
+ let mut s = String::from("hello world");
- //onmessage 监听服务器数据推送
- websocket.onmessage = function (evt) {
- msg.innerHTML += evt.data +'<br>';
-// console.log('Retrieved data from server: ' + evt.data);
- };
-//监听连接错误信息
-// websocket.onerror = function (evt, e) {
-// console.log('Error occured: ' + evt.data);
-// };
+ let word = first_word(&s);
-</script>
-</html>
+ s.clear(); // error!
-做了个循环,将当前用户的消息发送给同时在线的其他用户,比较简陋,如下图
user1:

-user2:
-
-user3:
-![QK8EU5`9TQNYIG_4YFU@DJN.png]()
+ println!("the first word is: {}", word);
+}
+那再执行 main 函数的时候就会抛错,因为 word 还是个切片,需要保证 s 的有效性,并且其实我们可以将函数申明成
+fn first_word(s: &str) -> &str {
+这样就既能处理&String 的情况,就是当成完整字符串的切片,也能处理普通的切片。
其他类型的切片
let a = [1, 2, 3, 4, 5];
+let slice = &a[1..3];
+简单记录下,具体可以去看看这本书
]]>wp_users 表,用 select 查询看下可以看到有用户的数据,如果是像我这样搭着玩的没有创建其他用户的话应该就只有一个用户,那我们的表里的用户数据就只会有一条,当然多条的话可以通过用户名来找UPDATE wp_users SET user_pass = MD5('123456') WHERE ID = 1;
+ 工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现
textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
+data = textFile.reduceByKey(lambda x, y: x + y).collect()
+for _ in data:
+ print(_)
-然后就能用自己的账户跟刚才更新的密码登录了。
+ +(3, 2)
+(1, 2)
+(4, 2)
+(2, 2)
+(5, 2)
]]>对于一块待整理区域,通过两个指针,free 在区域的起始端,scan 指针在区域的末端,free 指针从前往后知道找到空闲区域,scan 从后往前一直找到存活对象,当 free 指针未与 scan 指针交叉时,会给 scan 位置的对象特定位置标记上 free 的地址,即将要转移的地址,不过这里有个限制,这种整理算法一般会用于对象大小统一的情况,否则 free 指针扫描时还需要匹配scan 指针扫描到的存活对象的大小。
需要三次完整遍历堆区域
第一遍是遍历后将计算出所有对象的最终地址(转发地址)
第二遍是使用转发地址更新赋值器线程根以及被标记对象中的引用,该操作将确保它们指向对象的新位置
第三次遍历是relocate最终将存活对象移动到其新的目标位置
这个真的长见识了,
可以看到,原来是 A,B,C 对象引用了 N,这里会在第一次遍历的时候把这种引用反过来,让 N 的对象头部保存下 A 的地址,表示这类引用,然后在遍历到 B 的时候在链起来,到最后就会把所有引用了 N 对象的所有对象通过引线链起来,在第二次遍历的时候就把更新A,B,C 对象引用的 N 地址,并且移动 N 对象
这个一直提到过位图的实现方式,
可以看到在第一步会先通过位图标记,标记的方式是位图的每一位对应的堆内存的一个字(这里可能指的是 byte 吧),然后将一个存活对象的内存区域的第一个字跟最后一个字标记,这里如果在通过普通的方式就还需要一个地方在存转发地址,但是因为具体的位置可以通过位图算出来,也就不需要额外记录了
A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. SNI is currently supported by most modern browsers, though may not be used by some old or special clients.
来源
机翻一下:在单个 IP 地址上运行多个 HTTPS 服务器的更通用的解决方案是 TLS 服务器名称指示扩展(SNI,RFC 6066),它允许浏览器在 SSL 握手期间传递请求的服务器名称,因此,服务器将知道哪个 它应该用于连接的证书。 目前大多数现代浏览器都支持 SNI,但某些旧的或特殊的客户端可能不使用 SNI。
首先我们需要确认 sni 已被支持
在实际的配置中就可以这样
stream {
- map $ssl_preread_server_name $stream_map {
- nicksxs.me nme;
- nicksxs.com ncom;
- }
+ rust学习笔记-所有权一
+ /2021/04/18/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
+ 最近在看 《rust 权威指南》,还是难度比较大的,它里面的一些概念跟之前的用过的都有比较大的差别
比起有 gc 的虚拟机语言,跟像 C 和 C++这种主动释放内存的,rust 有他的独特点,主要是有三条
+
+- Rust中的每一个值都有一个对应的变量作为它的所有者。
+- 在同一时间内,值有且只有一个所有者。
+- 当所有者离开自己的作用域时,它持有的值就会被释放掉。
![]()
这里有两个重点:
+- s 在进入作用域后才变得有效
+- 它会保持自己的有效性直到自己离开作用域为止
+
+然后看个案例
+let x = 5;
+let y = x;
+这个其实有两种,一般可以认为比较多实现的会使用 copy on write 之类的,先让两个都指向同一个快 5 的存储,在发生变更后开始正式拷贝,但是涉及到内存处理的便利性,对于这类简单类型,可以直接拷贝
但是对于非基础类型
+let s1 = String::from("hello");
+let s2 = s1;
- upstream nme {
- server 127.0.0.1:8000;
- }
- upstream ncom {
- server 127.0.0.1:8001;
- }
+println!("{}, world!", s1);
+有可能认为有两种内存分布可能
先看下 string 的内存结构
![]()
第一种可能是
![]()
第二种是
![]()
我们来尝试编译下
![]()
发现有这个错误,其实在 rust 中let y = x这个行为的实质是移动,在赋值给 y 之后 x 就无效了
![]()
这样子就不会造成脱离作用域时,对同一块内存区域的二次释放,如果需要复制,可以使用 clone 方法
+let s1 = String::from("hello");
+let s2 = s1.clone();
- server {
- listen 443 reuseport;
- proxy_pass $stream_map;
- ssl_preread on;
- }
-}
-类似这样,但是这个理解是非常肤浅和不完善的,只是简单记忆下,后续再进行补充完整
-还有一点就是我们在配置的时候经常配置就是 server_name,但是会看到直接在使用 ssl_server_name,
其实在listen 标识了 ssl, 对应的 ssl_server_name 就等于 server_name,不需要额外处理了。
+println!("s1 = {}, s2 = {}", s1, s2);
+这里其实会有点疑惑,为什么前面的x, y 的行为跟 s1, s2 的不一样,其实主要是基本类型和 string 这类的不定大小的类型的内存分配方式不同,x, y这类整型可以直接确定大小,可以直接在栈上分配,而像 string 和其他的变体结构体,其大小都是不能在编译时确定,所以需要在堆上进行分配
此时我们就可以用引用来解决这个问题
+fn main() {
+ let s1 = String::from("hello");
+ let len = calculate_length(&s1);
+
+ println!("The length of '{}' is {}", s1, len);
+}
+fn calculate_length(s: &String) -> usize {
+ s.len()
+}
+这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值
由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃
而当我们尝试对引用的字符串进行修改时
+fn main() {
+ let s1 = String::from("hello");
+ change(&s1);
+}
+fn change(s: &String) {
+ s.push_str(", world");
+}
+就会有以下报错,
其实也很容易发现,毕竟没有 mut 指出这是可变引用,同时需要将 s1 改成 mut 可变的
fn main() {
+ let mut s1 = String::from("hello");
+ change(&mut s1);
+}
+
+
+fn change(s: &mut String) {
+ s.push_str(", world");
+}
+再看一个例子
+fn main() {
+ let mut s1 = String::from("hello");
+ let r1 = &mut s1;
+ let r2 = &mut s1;
+}
+这个例子在书里是会报错的,因为同时存在一个以上的可变引用,但是在我运行的版本里前面这段没有报错,只有当我真的要去更改的时候
+fn main() {
+ let mut s1 = String::from("hello");
+ let mut r1 = &mut s1;
+ let mut r2 = &mut s1;
+ change(&mut r1);
+ change(&mut r2);
+}
+
+
+fn change(s: &mut String) {
+ s.push_str(", world");
+}
+
这里可能就是具体版本在实现上的一个差异,我用的 rustc 是 1.44.0 版本
其实上面的主要是由 rust 想要避免这类多重可变更导致的异常问题,总结下就是三个点
还有一点需要注意的就是悬垂引用
+fn main() {
+ let reference_to_nothing = dangle();
+}
+
+fn dangle() -> &String {
+ let s = String::from("hello");
+ &s
+}
+这里可以看到其实在 dangle函数返回后,这里的 s 理论上就离开了作用域,但是由于返回了 s 的引用,在 main 函数中就会拿着这个引用,就会出现如下错误
最后总结下
+还是像我这样的小白专属,消息队列用来干啥,很多都是标准答案,用来削峰填谷的,这个完全对,只是我想结合场景说给像我这样的小白同学听,想想一个电商的下单功能,除了 AT 两家之外应该大部分都是接入的支付,那么下单支付完成后一般都是等支付回调,告诉你支付完成了(也有可能是失败了,或者超时了咱们主动去查),然后这个回调里我们自己的业务代码干点啥,首先比如是把订单状态改掉了,然后会有各类的操作,比如把优惠券核销了,把其他金钱相关的也核销了,把购物车里对应的商品给删了,还有更次要的,比如发个客服消息,让用户确认下地址的,给用户加积分的等等等等,想象下如果这些都是回调里一股脑儿做掉了,那可能你的代码健壮性跟相关服务的稳定性还有性能要达到一个非常高的水平才能让业务不出现异常,并且万一流量打起来了,这些重要的不重要的操作都会阻塞着,所以需要用一个消息队列,在接到回调后只处理极少的几个核心操作,完了就把这个消息丢进消息队列里,让各个业务方去消费这个消息,把客服消息发一下,给用户加个积分等等,这样子主要的业务流程需要处理的事情就少了,速度也加快了,这个例子呢不能严格算是削峰填谷的例子,不过也算是消息队列的比较典型的使用场景了,要说真实的削峰填谷的话其实可以这么理解,假如短时间内有 1w 个请求进来,系统能支持的 QPS 才 1000,那么正常情况下服务就挂了,或者被限流了,为了让服务正常,那么可以把这些请求先放进消息队列里,我服务端以拉的模式按我的处理能力来消费,这样就没啥问题了
-扯了这么多来聊聊 RocketMQ 长啥样
-
总共有四大部分:NameServer,Broker,Producer,Consumer。
-NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。
-NameServer压力不会太大,正常情况主要负责维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多,会导致一次心跳中,光Topic的数据就非常大,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败。
-Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。
-1.负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。
-2.动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。
---Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。
-
Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后想NameServer注册,Producer、Consumer通过NameServer发现新Broker,立即跟该Broker直连,收发消息。
3.高可用&高可靠
---高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。
-
高可靠:所有发往broker的消息,有同步刷盘和异步刷盘机制;同步刷盘时,消息写入物理文件才会返回成功,异步刷盘时,只有机器宕机,才会产生消息丢失,broker挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
RocketMQ提供三种发送方式:
--同步:在广泛的场景中使用可靠的同步传输,如重要的通知信息、短信通知、短信营销系统等。
-
异步:异步发送通常用于响应时间敏感的业务场景,发送出去即刻返回,利用回调做后续处理。
一次性:一次性发送用于需要中等可靠性的情况,如日志收集,发送出去即完成,不用等待发送结果,回调等等。
生产者发送时,会自动轮询当前所有可发送的broker,一条消息发送成功,下次换另外一个broker发送,以达到消息平均落到所有的broker上。
-Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。
-先讨论消费者的消费模式,消费者有两种模式消费:集群消费,广播消费。
---广播消费:每个消费者消费Topic下的所有队列。
-
集群消费:一个topic可以由同一个ID下所有消费者分担消费。
具体例子:假如TopicA有6个队列,某个消费者ID起了2个消费者实例,那么每个消费者负责消费3个队列。如果再增加一个消费者ID相同消费者实例,即当前共有3个消费者同时消费6个队列,那每个消费者负责2个队列的消费。
消费者端的负载均衡,就是集群消费模式下,同一个ID的所有消费者实例平均消费该Topic的所有队列。
-消费者从用户角度来看有两种类型:
---PullConsumer:主动从brokers处拉取消息。Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
-
PushConsumer:Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。
Topic:主题,表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。Topic与生产者和消费者都是非常松散的关系,一个topic可以有0个或者1个或者多个生产者向其发送消息,换句话说,一个生产者可以同时向不同和topic发送消息。从消费者的解度来说,一个topic可能被0个或者一个或者多个消费组订阅,类似的,一个消费组可以订阅一个或者多个主题只要这个消费组的实例保持他们的订阅一致。
-Message:消息消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。。
-Message Queue:消息队列,一个主题被化分为一个或者多个子主题(sub-topics),“消息队列”.
-Tag:标签,为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。使用tag,同一业务模块不同目的的messages就可以用相同topic不同tag来标识。Tags有益于保持你的代码干净而条理清晰,同时促进使用RocketMQ提供的查询系统的效率。Topic:主题,是生产者发送的消息和消费者拉取的消息的归类。Topic与生产者和消费者都是非常松散的关系,一个topic可以有0个或者1个或者多个生产者向其发送消息,换句话说,一个生产者可以同时向不同和topic发送消息。从消费者的解度来说,一个topic可能被0个或者一个或者多个消费组订阅,类似的,一个消费组可以订阅一个或者多个主题只要这个消费组的实例保持他们的订阅一致。
-Message Order:当使用DefaultMQPushConsumer时,你需要确定消费消息的方式:
---Orderly:顺序地消费消息即表示消费的消息顺序同生产者发送的顺序一致。
-
Concurrently:并行消费。指定此方式消费,信息消费的最大并行数量仅受限于每个消费者客户端指定的线程池。
Consumer Group:消费组,同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
Producer Group:生产者组,同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
上面的这些我主要参考了 RocketMQ 的 GitHub 介绍和一些优秀网文的介绍,侵权请联系我删除。
+bind: Cannot assign requested address
+查了下这个问题,猜测是不是端口已经被占用了,查了下并不是,然后想到是不是端口是系统保留的,
+sysctl -a |grep port_range
+结果中
+net.ipv4.ip_local_port_range = 50000 65000 -----意味着50000~65000端口可用
+发现也不是,没有限制,最后才查到这个原因是默认如果有 ipv6 的话会使用 ipv6 的地址做映射
所以如果是命令连接做端口转发的话,
ssh -4 -L 11234:localhost:1234 x.x.x.x
+使用-4来制定通过 ipv4 地址来做映射
如果是在 .ssh/config 中配置的话可以直接指定所有的连接都走 ipv4
Host *
+ AddressFamily inet
+inet代表 ipv4,inet6代表 ipv6
AddressFamily 的所有取值范围是:”any”(默认)、”inet”(仅IPv4)、”inet6”(仅IPv6)。
另外此类问题还可以通过 ssh -v 来打印更具体的信息
在日常使用云服务器的时候,如果要访问上面自建的 mysql,一般要不直接开对应的端口,然后需要对本地 ip 进行授权,但是这个方案会有比较多的限制,比如本地 ip 变了,比如是非固定出口 ip 的家用宽带,或者要在家里跟公司都要访问,如果对所有 ip 都授权的话会不安全,这个时候其实是用 ssh 端口转发是个比较安全方便的方式。
原来在这个之前其实对这块内容不太了解,后面是听朋友说的,vscode 的 Remote - SSH 扩展可以很方便的使用端口转发,在使用该扩展的时候,会在控制台位置里都出现一个”端口” tab
如图中所示,我就是将一个服务器上的 mysql 的 3306 端口转发到本地的 3307 端口,至于为什么不用 3306 是因为本地我也有个 mysql 已经使用了 3306 端口,这个方法是使用的 vscode 的这个扩展,
还有个方式是直接使用 ssh 命令
命令可以如此
ssh -CfNg -L 3307:127.0.0.1:3306 user1@199.199.199.199
-简单介绍下这个命令-C 表示的是压缩数据包-f 表示后台执行命令-N 是表示不执行具体命令只用于端口转发-g 表示允许远程主机连接本地转发端口-L 则是具体端口转发的映射配置
上面的命令就是将远程主机的 127.0.0.1:3306 对应转发到本地 3307
而后面的用户则就是登录主机的用户名user1和ip地址199.199.199.199,当然这个配置也不是唯一的
还可以在ssh 的 config 配置中加对应的配置
-Host host1
- HostName 199.199.199.199
- User user1
- IdentityFile /Users/user1/.ssh/id_rsa
- ServerAliveInterval 60
- LocalForward 3310 127.0.0.1:3306
-然后通过 ssh host1 连接服务器的时候就能顺带做端口转发
-]]>--MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
-
mybatis一大特点,或者说比较为人熟知的应该就是比 hibernate 是更轻量化,为国人所爱好的orm框架,对于hibernate目前还没有深入的拆解过,后续可以也写一下,在使用体验上觉得是个比较精巧的框架,看代码也比较容易,所以就想写个系列,第一篇先是介绍下使用
根据官网的文档上我们先来尝试一下简单使用
首先我们有个简单的配置,这个文件是mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE configuration
- PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
- "https://mybatis.org/dtd/mybatis-3-config.dtd">
-<configuration>
- <!-- 需要加入的properties-->
- <properties resource="application-development.properties"/>
- <!-- 指出使用哪个环境,默认是development-->
- <environments default="development">
- <environment id="development">
- <!-- 指定事务管理器类型-->
- <transactionManager type="JDBC"/>
- <!-- 指定数据源类型-->
- <dataSource type="POOLED">
- <!-- 下面就是具体的参数占位了-->
- <property name="driver" value="${driver}"/>
- <property name="url" value="${url}"/>
- <property name="username" value="${username}"/>
- <property name="password" value="${password}"/>
- </dataSource>
- </environment>
- </environments>
- <mappers>
- <!-- 指定mapper xml的位置或文件-->
- <mapper resource="mapper/StudentMapper.xml"/>
- </mappers>
-</configuration>
-在代码里创建mybatis里重要入口
-String resource = "mybatis-config.xml";
-InputStream inputStream = Resources.getResourceAsStream(resource);
-SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-然后我们上面的StudentMapper.xml
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper
- PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.nicksxs.mybatisdemo.StudentMapper">
- <select id="selectStudent" resultType="com.nicksxs.mybatisdemo.StudentDO">
- select * from student where id = #{id}
- </select>
-</mapper>
-那么我们就要使用这个mapper,
-String resource = "mybatis-config.xml";
-InputStream inputStream = Resources.getResourceAsStream(resource);
-SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-try (SqlSession session = sqlSessionFactory.openSession()) {
- StudentDO studentDO = session.selectOne("com.nicksxs.mybatisdemo.StudentMapper.selectStudent", 1);
- System.out.println("id is " + studentDO.getId() + " name is " +studentDO.getName());
-} catch (Exception e) {
- e.printStackTrace();
-}
-sqlSessionFactory是sqlSession的工厂,我们可以通过sqlSessionFactory来创建sqlSession,而SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。可以看到mapper.xml中有定义mapper的namespace,就可以通过session.selectOne()传入namespace+id来调用这个方法
但是这样调用比较不合理的点,或者说按后面mybatis优化之后我们可以指定mapper接口
public interface StudentMapper {
+ spring event 介绍
+ /2022/01/30/spring-event-%E4%BB%8B%E7%BB%8D/
+ spring框架中如果想使用一些一部操作,除了依赖第三方中间件的消息队列,还可以用spring自己的event,简单介绍下使用方法
首先我们可以建一个event,继承ApplicationEvent
+
+public class CustomSpringEvent extends ApplicationEvent {
- public StudentDO selectStudent(Long id);
-}
-就可以可以通过mapper接口获取方法,这样就不用涉及到未知的变量转换等异常
-try (SqlSession session = sqlSessionFactory.openSession()) {
- StudentMapper mapper = session.getMapper(StudentMapper.class);
- StudentDO studentDO = mapper.selectStudent(1L);
- System.out.println("id is " + studentDO.getId() + " name is " +studentDO.getName());
-} catch (Exception e) {
- e.printStackTrace();
+ private String message;
+
+ public CustomSpringEvent(Object source, String message) {
+ super(source);
+ this.message = message;
+ }
+ public String getMessage() {
+ return message;
+ }
+}
+这个 ApplicationEvent 其实也比较简单,内部就一个 Object 类型的 source,可以自行扩展,我们在自定义的这个 Event 里加了个 Message ,只是简单介绍下使用
+public abstract class ApplicationEvent extends EventObject {
+ private static final long serialVersionUID = 7099057708183571937L;
+ private final long timestamp;
+
+ public ApplicationEvent(Object source) {
+ super(source);
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ public ApplicationEvent(Object source, Clock clock) {
+ super(source);
+ this.timestamp = clock.millis();
+ }
+
+ public final long getTimestamp() {
+ return this.timestamp;
+ }
+}
+
+然后就是事件生产者和监听消费者
+@Component
+public class CustomSpringEventPublisher {
+
+ @Resource
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ public void publishCustomEvent(final String message) {
+ System.out.println("Publishing custom event. ");
+ CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
+ applicationEventPublisher.publishEvent(customSpringEvent);
+ }
+}
+这里的 ApplicationEventPublisher 是 Spring 的方法接口
+@FunctionalInterface
+public interface ApplicationEventPublisher {
+ default void publishEvent(ApplicationEvent event) {
+ this.publishEvent((Object)event);
+ }
+
+ void publishEvent(Object var1);
+}
+具体的是例如 org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType) 中的实现,后面可以展开讲讲
+事件监听者:
+@Component
+public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
+ @Override
+ public void onApplicationEvent(CustomSpringEvent event) {
+ System.out.println("Received spring custom event - " + event.getMessage());
+ }
}
-这一篇咱们先介绍下简单的使用,后面可以先介绍下这些的原理。
+这里的也是 spring 的一个方法接口
+@FunctionalInterface
+public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
+ void onApplicationEvent(E var1);
+
+ static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
+ return (event) -> {
+ consumer.accept(event.getPayload());
+ };
+ }
+}
+
+然后简单包个请求
+
+@RequestMapping(value = "/event", method = RequestMethod.GET)
+@ResponseBody
+public void event() {
+ customSpringEventPublisher.publishCustomEvent("hello sprint event");
+}
+
+![]()
就能看到接收到消息了。
]]>
Java
- Mybatis
+ Spring
Java
- Mysql
- Mybatis
+ Spring
+ Spring Event
此时我们就可以用引用来解决这个问题
-fn main() {
- let s1 = String::from("hello");
- let len = calculate_length(&s1);
+ swoole-websocket-test
+ /2016/07/13/swoole-websocket-test/
+ 玩一下swoole的websocketWebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。
,在web私信,im等应用较多。背景和优缺点可以参看wiki。
+环境准备
因为swoole官方还不支持windows,所以需要装下linux,之前都是用ubuntu,
这次就试一下centos7,还是满好看的,虽然虚拟机会默认最小安装,需要在安装
时自己选择带gnome的,当然最小安装也是可以的,只是最后需要改下防火墙。
然后是装下PHP,Nginx什么的,我是用Oneinstack,可以按需安装
给做这个的大大点个赞。
+
- println!("The length of '{}' is {}", s1, len);
-}
-fn calculate_length(s: &String) -> usize {
- s.len()
-}
-这里的&符号就是引用的语义,它们允许你在不获得所有权的前提下使用值
由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃
而当我们尝试对引用的字符串进行修改时
-fn main() {
- let s1 = String::from("hello");
- change(&s1);
-}
-fn change(s: &String) {
- s.push_str(", world");
-}
-就会有以下报错,
其实也很容易发现,毕竟没有 mut 指出这是可变引用,同时需要将 s1 改成 mut 可变的
fn main() {
- let mut s1 = String::from("hello");
- change(&mut s1);
-}
+swoole
1.install via pecl
+pecl install swoole
+2.install from source
+sudo apt-get install php5-dev
+git clone https://github.com/swoole/swoole-src.git
+cd swoole-src
+phpize
+./configure
+make && make install
+3.add extension
+extension = swoole.so
-fn change(s: &mut String) {
- s.push_str(", world");
-}
-再看一个例子
-fn main() {
- let mut s1 = String::from("hello");
- let r1 = &mut s1;
- let r2 = &mut s1;
-}
-这个例子在书里是会报错的,因为同时存在一个以上的可变引用,但是在我运行的版本里前面这段没有报错,只有当我真的要去更改的时候
-fn main() {
- let mut s1 = String::from("hello");
- let mut r1 = &mut s1;
- let mut r2 = &mut s1;
- change(&mut r1);
- change(&mut r2);
-}
+4.test extension
+php -m | grep swoole
+如果存在就代表安装成功啦
+Exec
实现代码看了这位仁兄的代码
+还是贴一下代码
服务端:
+//创建websocket服务器对象,监听0.0.0.0:9502端口
+$ws = new swoole_websocket_server("0.0.0.0", 9502);
+//监听WebSocket连接打开事件
+$ws->on('open', function ($ws, $request) {
+ $fd[] = $request->fd;
+ $GLOBALS['fd'][] = $fd;
+ //区别下当前用户
+ $ws->push($request->fd, "hello user{$request->fd}, welcome\n");
+});
-fn change(s: &mut String) {
- s.push_str(", world");
-}
-![]()
这里可能就是具体版本在实现上的一个差异,我用的 rustc 是 1.44.0 版本
其实上面的主要是由 rust 想要避免这类多重可变更导致的异常问题,总结下就是三个点
-
-- 两个或两个以上的指针同时同时访问同一空间
-- 其中至少有一个指针会想空间中写入数据
-- 没有同步数据访问的机制
并且我们不能在拥有不可变引用的情况下创建可变引用
-
-悬垂引用
还有一点需要注意的就是悬垂引用
-fn main() {
- let reference_to_nothing = dangle();
-}
+//监听WebSocket消息事件
+$ws->on('message', function ($ws, $frame) {
+ $msg = 'from'.$frame->fd.":{$frame->data}\n";
-fn dangle() -> &String {
- let s = String::from("hello");
- &s
-}
-这里可以看到其实在 dangle函数返回后,这里的 s 理论上就离开了作用域,但是由于返回了 s 的引用,在 main 函数中就会拿着这个引用,就会出现如下错误
![]()
-总结
最后总结下
-
-- 在任何一个段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
-- 引用总是有效的。
-
+ foreach($GLOBALS['fd'] as $aa){
+ foreach($aa as $i){
+ if($i != $frame->fd) {
+ # code...
+ $ws->push($i,$msg);
+ }
+ }
+ }
+});
+
+//监听WebSocket连接关闭事件
+$ws->on('close', function ($ws, $fd) {
+ echo "client-{$fd} is closed\n";
+});
+
+$ws->start();
+
+客户端:
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Title</title>
+</head>
+<body>
+<div id="msg"></div>
+<input type="text" id="text">
+<input type="submit" value="发送数据" onclick="song()">
+</body>
+<script>
+ var msg = document.getElementById("msg");
+ var wsServer = 'ws://0.0.0.0:9502';
+ //调用websocket对象建立连接:
+ //参数:ws/wss(加密)://ip:port (字符串)
+ var websocket = new WebSocket(wsServer);
+ //onopen监听连接打开
+ websocket.onopen = function (evt) {
+ //websocket.readyState 属性:
+ /*
+ CONNECTING 0 The connection is not yet open.
+ OPEN 1 The connection is open and ready to communicate.
+ CLOSING 2 The connection is in the process of closing.
+ CLOSED 3 The connection is closed or couldn't be opened.
+ */
+ msg.innerHTML = websocket.readyState;
+ };
+
+ function song(){
+ var text = document.getElementById('text').value;
+ document.getElementById('text').value = '';
+ //向服务器发送数据
+ websocket.send(text);
+ }
+ //监听连接关闭
+// websocket.onclose = function (evt) {
+// console.log("Disconnected");
+// };
+
+ //onmessage 监听服务器数据推送
+ websocket.onmessage = function (evt) {
+ msg.innerHTML += evt.data +'<br>';
+// console.log('Retrieved data from server: ' + evt.data);
+ };
+//监听连接错误信息
+// websocket.onerror = function (evt, e) {
+// console.log('Error occured: ' + evt.data);
+// };
+
+</script>
+</html>
+
+做了个循环,将当前用户的消息发送给同时在线的其他用户,比较简陋,如下图
user1:
+user2:
+
+user3:
+
工作中学习使用了一下Spark做数据分析,主要是用spark的python接口,首先是pyspark.SparkContext(appName=xxx),这是初始化一个Spark应用实例或者说会话,不能重复,
返回的实例句柄就可以调用textFile(path)读取文本文件,这里的文本文件可以是HDFS上的文本文件,也可以普通文本文件,但是需要在Spark的所有集群上都存在,否则会
读取失败,parallelize则可以将python生成的集合数据读取后转换成rdd(A Resilient Distributed Dataset (RDD),一种spark下的基本抽象数据集),基于这个RDD就可以做
数据的流式计算,例如map reduce,在Spark中可以非常方便地实现
textFile = sc.parallelize([(1,1), (2,1), (3,1), (4,1), (5,1),(1,1), (2,1), (3,1), (4,1), (5,1)])
-data = textFile.reduceByKey(lambda x, y: x + y).collect()
-for _ in data:
- print(_)
-
-
-(3, 2)
-(1, 2)
-(4, 2)
-(2, 2)
-(5, 2)
+ 先聊聊这个事情,整体看下来我的一些理解,IPCC给中国的方案其实是个很大的陷阱,它里面有几个隐藏的点是容易被我们外行忽略的,第一点是基数,首先发达国家目前(指2010年采访或者IPCC方案时间)的人均碳排放量已经是远高于发展中国家的了,这也就导致了所谓的发达国家承诺减排80%是个非常有诚意的承诺其实就是忽悠;第二点是碳排放是个累计过程,从1900年开始到2050年,或者说到2010年,发达国家已经排的量是远超过发展中国家的,这是非常不公平的;第三点其实是通过前两点推导出来的,也就是即使发达国家这么有诚意地说减排80%,扒开这层虚伪的外衣,其实是给他们11亿人分走了48%的碳排放量,留给发展中国家55亿人口的只剩下了52%;第四点,人是否因为国家的发达与否而应受到不平等待遇,如果按国家维度,丁老说的,摩纳哥要跟中国分同样的排放量么,中国人还算不算人;第五点,这点算是我自己想的,也可能是柴静屁股决定脑袋想不到的点,她作为一个物质生活条件已经足够好了,那么对于那些生活在物质条件平均线以下的,他们是否能像城里人那样有空调地暖,洗澡有热水器浴霸,上下班能开车,这些其实都直接或者间接地导致了碳排放;他们有没有改善物质生活条件地权利呢,并且再说回来,其实丁老也给了我们觉得合理地方案,我们保证不管发达国家不管减排多少,我们都不会超过他们的80%,我觉得这是真正的诚意,而不是接着减排80%的噱头来忽悠人,也是像丁老这样的专家才能看破这个陷阱,碳排放权其实就是发展权,就是人权,中国人就不是人了么,或者说站在贫困线以下的人民是否有改善物质条件的权利,而不是说像柴静这样,只是管她自己,可能觉得小孩因为空气污染导致身体不好,所以做了穹顶之下这个纪录片,想去改善这个事情,空气污染不是说对的,只是每个国家都有这个过程,如果不发展,哪里有资源去让人活得好,活得好了是前提,然后再去各方面都改善。
-对于这个问题其实更想说的是人的认知偏差,之前总觉得美帝是更自由民主,公平啥的,或者说觉得美帝啥都好,有种无脑愤青的感觉,外国的月亮比较圆,但是经历了像川普当选美国总统以来的各种魔幻操作,还有对于疫情的种种不可思议的美国民众的反应,其实更让人明白第一是外国的月亮没比较圆,第二是事情总是没那么简单粗暴非黑即白,美国不像原先设想地那么领先优秀,但是的确有很多方面是全球领先的,天朝也有体制所带来的优势,不可妄自菲薄,也不能忙不自大,还是要多学习知识,提升认知水平。
-]]>Given a sorted integer array without duplicates, return the summary of its ranges.
+For example, given [0,1,2,4,5,7], return ["0->2","4->5","7"].
每一个区间的起点nums[i]加上j是否等于nums[i+j]
参考
class Solution {
+public:
+ vector<string> summaryRanges(vector<int>& nums) {
+ int i = 0, j = 1, n;
+ vector<string> res;
+ n = nums.size();
+ while(i < n){
+ j = 1;
+ while(j < n && nums[i+j] - nums[i] == j) j++;
+ res.push_back(j <= 1 ? to_string(nums[i]) : to_string(nums[i]) + "->" + to_string(nums[i + j - 1]));
+ i += j;
+ }
+ return res;
+ }
+};]]>wp_users 表,用 select 查询看下可以看到有用户的数据,如果是像我这样搭着玩的没有创建其他用户的话应该就只有一个用户,那我们的表里的用户数据就只会有一条,当然多条的话可以通过用户名来找UPDATE wp_users SET user_pass = MD5('123456') WHERE ID = 1;
+
+然后就能用自己的账户跟刚才更新的密码登录了。
]]>A more generic solution for running several HTTPS servers on a single IP address is TLS Server Name Indication extension (SNI, RFC 6066), which allows a browser to pass a requested server name during the SSL handshake and, therefore, the server will know which certificate it should use for the connection. SNI is currently supported by most modern browsers, though may not be used by some old or special clients.
来源
机翻一下:在单个 IP 地址上运行多个 HTTPS 服务器的更通用的解决方案是 TLS 服务器名称指示扩展(SNI,RFC 6066),它允许浏览器在 SSL 握手期间传递请求的服务器名称,因此,服务器将知道哪个 它应该用于连接的证书。 目前大多数现代浏览器都支持 SNI,但某些旧的或特殊的客户端可能不使用 SNI。
首先我们需要确认 sni 已被支持
在实际的配置中就可以这样
stream {
+ map $ssl_preread_server_name $stream_map {
+ nicksxs.me nme;
+ nicksxs.com ncom;
+ }
+
+ upstream nme {
+ server 127.0.0.1:8000;
+ }
+ upstream ncom {
+ server 127.0.0.1:8001;
+ }
+
+ server {
+ listen 443 reuseport;
+ proxy_pass $stream_map;
+ ssl_preread on;
+ }
+}
+类似这样,但是这个理解是非常肤浅和不完善的,只是简单记忆下,后续再进行补充完整
+还有一点就是我们在配置的时候经常配置就是 server_name,但是会看到直接在使用 ssl_server_name,
其实在listen 标识了 ssl, 对应的 ssl_server_name 就等于 server_name,不需要额外处理了。
0xc00000e9 错误码,现在想着是不是这个固态硬盘有点问题或者还是内存问题,只能后续再看了,拆机的时候也找不到视频,机器实在是太老了,后面再试试看。
+ 对于一块待整理区域,通过两个指针,free 在区域的起始端,scan 指针在区域的末端,free 指针从前往后知道找到空闲区域,scan 从后往前一直找到存活对象,当 free 指针未与 scan 指针交叉时,会给 scan 位置的对象特定位置标记上 free 的地址,即将要转移的地址,不过这里有个限制,这种整理算法一般会用于对象大小统一的情况,否则 free 指针扫描时还需要匹配scan 指针扫描到的存活对象的大小。
需要三次完整遍历堆区域
第一遍是遍历后将计算出所有对象的最终地址(转发地址)
第二遍是使用转发地址更新赋值器线程根以及被标记对象中的引用,该操作将确保它们指向对象的新位置
第三次遍历是relocate最终将存活对象移动到其新的目标位置
这个真的长见识了,
可以看到,原来是 A,B,C 对象引用了 N,这里会在第一次遍历的时候把这种引用反过来,让 N 的对象头部保存下 A 的地址,表示这类引用,然后在遍历到 B 的时候在链起来,到最后就会把所有引用了 N 对象的所有对象通过引线链起来,在第二次遍历的时候就把更新A,B,C 对象引用的 N 地址,并且移动 N 对象
这个一直提到过位图的实现方式,
可以看到在第一步会先通过位图标记,标记的方式是位图的每一位对应的堆内存的一个字(这里可能指的是 byte 吧),然后将一个存活对象的内存区域的第一个字跟最后一个字标记,这里如果在通过普通的方式就还需要一个地方在存转发地址,但是因为具体的位置可以通过位图算出来,也就不需要额外记录了
还是像我这样的小白专属,消息队列用来干啥,很多都是标准答案,用来削峰填谷的,这个完全对,只是我想结合场景说给像我这样的小白同学听,想想一个电商的下单功能,除了 AT 两家之外应该大部分都是接入的支付,那么下单支付完成后一般都是等支付回调,告诉你支付完成了(也有可能是失败了,或者超时了咱们主动去查),然后这个回调里我们自己的业务代码干点啥,首先比如是把订单状态改掉了,然后会有各类的操作,比如把优惠券核销了,把其他金钱相关的也核销了,把购物车里对应的商品给删了,还有更次要的,比如发个客服消息,让用户确认下地址的,给用户加积分的等等等等,想象下如果这些都是回调里一股脑儿做掉了,那可能你的代码健壮性跟相关服务的稳定性还有性能要达到一个非常高的水平才能让业务不出现异常,并且万一流量打起来了,这些重要的不重要的操作都会阻塞着,所以需要用一个消息队列,在接到回调后只处理极少的几个核心操作,完了就把这个消息丢进消息队列里,让各个业务方去消费这个消息,把客服消息发一下,给用户加个积分等等,这样子主要的业务流程需要处理的事情就少了,速度也加快了,这个例子呢不能严格算是削峰填谷的例子,不过也算是消息队列的比较典型的使用场景了,要说真实的削峰填谷的话其实可以这么理解,假如短时间内有 1w 个请求进来,系统能支持的 QPS 才 1000,那么正常情况下服务就挂了,或者被限流了,为了让服务正常,那么可以把这些请求先放进消息队列里,我服务端以拉的模式按我的处理能力来消费,这样就没啥问题了
+扯了这么多来聊聊 RocketMQ 长啥样
+
总共有四大部分:NameServer,Broker,Producer,Consumer。
+NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。
+NameServer压力不会太大,正常情况主要负责维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多,会导致一次心跳中,光Topic的数据就非常大,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败。
+Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。
+1.负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。
+2.动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。
+++Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。
+
Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后想NameServer注册,Producer、Consumer通过NameServer发现新Broker,立即跟该Broker直连,收发消息。
3.高可用&高可靠
+++高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。
+
高可靠:所有发往broker的消息,有同步刷盘和异步刷盘机制;同步刷盘时,消息写入物理文件才会返回成功,异步刷盘时,只有机器宕机,才会产生消息丢失,broker挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
RocketMQ提供三种发送方式:
++同步:在广泛的场景中使用可靠的同步传输,如重要的通知信息、短信通知、短信营销系统等。
+
异步:异步发送通常用于响应时间敏感的业务场景,发送出去即刻返回,利用回调做后续处理。
一次性:一次性发送用于需要中等可靠性的情况,如日志收集,发送出去即完成,不用等待发送结果,回调等等。
生产者发送时,会自动轮询当前所有可发送的broker,一条消息发送成功,下次换另外一个broker发送,以达到消息平均落到所有的broker上。
+Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。
+先讨论消费者的消费模式,消费者有两种模式消费:集群消费,广播消费。
+++广播消费:每个消费者消费Topic下的所有队列。
+
集群消费:一个topic可以由同一个ID下所有消费者分担消费。
具体例子:假如TopicA有6个队列,某个消费者ID起了2个消费者实例,那么每个消费者负责消费3个队列。如果再增加一个消费者ID相同消费者实例,即当前共有3个消费者同时消费6个队列,那每个消费者负责2个队列的消费。
消费者端的负载均衡,就是集群消费模式下,同一个ID的所有消费者实例平均消费该Topic的所有队列。
+消费者从用户角度来看有两种类型:
+++PullConsumer:主动从brokers处拉取消息。Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
+
PushConsumer:Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。
Topic:主题,表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。Topic与生产者和消费者都是非常松散的关系,一个topic可以有0个或者1个或者多个生产者向其发送消息,换句话说,一个生产者可以同时向不同和topic发送消息。从消费者的解度来说,一个topic可能被0个或者一个或者多个消费组订阅,类似的,一个消费组可以订阅一个或者多个主题只要这个消费组的实例保持他们的订阅一致。
+Message:消息消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。。
+Message Queue:消息队列,一个主题被化分为一个或者多个子主题(sub-topics),“消息队列”.
+Tag:标签,为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。使用tag,同一业务模块不同目的的messages就可以用相同topic不同tag来标识。Tags有益于保持你的代码干净而条理清晰,同时促进使用RocketMQ提供的查询系统的效率。Topic:主题,是生产者发送的消息和消费者拉取的消息的归类。Topic与生产者和消费者都是非常松散的关系,一个topic可以有0个或者1个或者多个生产者向其发送消息,换句话说,一个生产者可以同时向不同和topic发送消息。从消费者的解度来说,一个topic可能被0个或者一个或者多个消费组订阅,类似的,一个消费组可以订阅一个或者多个主题只要这个消费组的实例保持他们的订阅一致。
+Message Order:当使用DefaultMQPushConsumer时,你需要确定消费消息的方式:
+++Orderly:顺序地消费消息即表示消费的消息顺序同生产者发送的顺序一致。
+
Concurrently:并行消费。指定此方式消费,信息消费的最大并行数量仅受限于每个消费者客户端指定的线程池。
Consumer Group:消费组,同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
Producer Group:生产者组,同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
上面的这些我主要参考了 RocketMQ 的 GitHub 介绍和一些优秀网文的介绍,侵权请联系我删除。
+]]>先聊聊这个事情,整体看下来我的一些理解,IPCC给中国的方案其实是个很大的陷阱,它里面有几个隐藏的点是容易被我们外行忽略的,第一点是基数,首先发达国家目前(指2010年采访或者IPCC方案时间)的人均碳排放量已经是远高于发展中国家的了,这也就导致了所谓的发达国家承诺减排80%是个非常有诚意的承诺其实就是忽悠;第二点是碳排放是个累计过程,从1900年开始到2050年,或者说到2010年,发达国家已经排的量是远超过发展中国家的,这是非常不公平的;第三点其实是通过前两点推导出来的,也就是即使发达国家这么有诚意地说减排80%,扒开这层虚伪的外衣,其实是给他们11亿人分走了48%的碳排放量,留给发展中国家55亿人口的只剩下了52%;第四点,人是否因为国家的发达与否而应受到不平等待遇,如果按国家维度,丁老说的,摩纳哥要跟中国分同样的排放量么,中国人还算不算人;第五点,这点算是我自己想的,也可能是柴静屁股决定脑袋想不到的点,她作为一个物质生活条件已经足够好了,那么对于那些生活在物质条件平均线以下的,他们是否能像城里人那样有空调地暖,洗澡有热水器浴霸,上下班能开车,这些其实都直接或者间接地导致了碳排放;他们有没有改善物质生活条件地权利呢,并且再说回来,其实丁老也给了我们觉得合理地方案,我们保证不管发达国家不管减排多少,我们都不会超过他们的80%,我觉得这是真正的诚意,而不是接着减排80%的噱头来忽悠人,也是像丁老这样的专家才能看破这个陷阱,碳排放权其实就是发展权,就是人权,中国人就不是人了么,或者说站在贫困线以下的人民是否有改善物质条件的权利,而不是说像柴静这样,只是管她自己,可能觉得小孩因为空气污染导致身体不好,所以做了穹顶之下这个纪录片,想去改善这个事情,空气污染不是说对的,只是每个国家都有这个过程,如果不发展,哪里有资源去让人活得好,活得好了是前提,然后再去各方面都改善。
+对于这个问题其实更想说的是人的认知偏差,之前总觉得美帝是更自由民主,公平啥的,或者说觉得美帝啥都好,有种无脑愤青的感觉,外国的月亮比较圆,但是经历了像川普当选美国总统以来的各种魔幻操作,还有对于疫情的种种不可思议的美国民众的反应,其实更让人明白第一是外国的月亮没比较圆,第二是事情总是没那么简单粗暴非黑即白,美国不像原先设想地那么领先优秀,但是的确有很多方面是全球领先的,天朝也有体制所带来的优势,不可妄自菲薄,也不能忙不自大,还是要多学习知识,提升认知水平。
+]]>range revert来进行 git revert, 用法就是
-git revert OLDER_COMMIT^..NEWER_COMMIT
-这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明
然后就可以用 -n 参数,表示不自动提交
git revert -n OLDER_COMMIT^..NEWER_COMMIT
-git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"
-
-
+ 最近这次经历也是有火绒的一定责任,在我尝试推出 U盘的时候提示了我被另一个大流氓软件,XlibabaProtect.exe 占用了,这个流氓软件真的是充分展示了某里的技术实力,试过 N 多种办法都关不掉也删不掉,尝试了很多种办法也没办法删除,但是后面换了种思路,一般这种情况肯定是有进程在占用 U盘里的内容,最新版本的 Powertoys 会在文件的右键菜单里添加一个叫 File Locksmith 的功能,可以用于检查正在使用哪些文件以及由哪些进程使用,但是可能是我的使用姿势不对,没有仔细看文档,它里面有个”以管理员身份重启”,可能会有用。
这算是第一种方式,
第二种方式是 Windows 任务管理器中性能 tab 下的”打开资源监视器”,
,假如我的 U 盘的盘符是F:
就可以搜索到占用这个盘符下文件的进程,这里千万小心‼️‼️,不可轻易杀掉这些进程,有些系统进程如果轻易杀掉会导致蓝屏等问题,不可轻易尝试,除非能确认这些进程的作用。
对于前两种方式对我来说都无效,
所以尝试了第三种,
就是磁盘脱机的方式,在”计算机”右键管理,点击”磁盘管理”,可以找到 U 盘盘符右键,点击”脱机”,然后再”推出”,这个对我来说也不行
这种是唯一对我有效的,在开始菜单搜索”event”,可以搜到”事件查看器”,
,这个可以看到当前最近 Windows 发生的事件,打开这个后就点击U盘推出,因为推不出来也是一种错误事件,点击下刷新就能在这看到具体是因为什么推出不了,具体的进程信息
最后发现是英特尔的驱动管理程序的一个进程,关掉就退出了,虽然前面说的某里的进程是流氓,但这边是真的冤枉它了
range revert来进行 git revert, 用法就是
+git revert OLDER_COMMIT^..NEWER_COMMIT
+这样就可以解决上面的问题了,但是还有个问题是这样会根据前面的 commit 数量提交对应数量个 revert commit 会显得比较乱,如果要比较干净的 commit 历史,
可以看下 git revert 命令说明
然后就可以用 -n 参数,表示不自动提交
git revert -n OLDER_COMMIT^..NEWER_COMMIT
+git commit -m "revert OLDER_COMMIT to NEWER_COMMIT"
+
+
+]]>0xc00000e9 错误码,现在想着是不是这个固态硬盘有点问题或者还是内存问题,只能后续再看了,拆机的时候也找不到视频,机器实在是太老了,后面再试试看。
+]]>时光飞逝,我在初中高中的时候因为爱打篮球,以为自己体质已经有了质的变化,所以在体育课跑步的时候妄图跟一位体育非常好的同学一起跑,结果跟的快断气了,最终还是确认了自己是个体育渣,特别是到了大学的第一次体测跑一千米,跑完直接吐了,一则是大学太宅不运动,二则的确是底子不好。那么怎么会去跑步了呢,其实也没什么特殊的原因,就是工作以后因为运动得更少,体质差,而且越来越胖,所以就想运动下,加之跑步也是我觉得成本最低的运动了,刚好那时候17 年租的地方附近小区周围的路车不太多,一圈刚好一公里多,就觉得开始跑跑看,其实想想以前觉得一千米是非常远的,学校塑胶跑道才 400 米,一千米要两圈半,太难了,但是后来在这个小区周围跑的时候好像跑了一圈以后还能再跑一点,最后跑了两圈,可把自己牛坏了,我都能跑两千米了,哈哈,这是个什么概念呢,大学里最让我绝望的两项体育相关的内容就是一千米和十二分钟跑,一千米把我跑吐了,十二分钟跑及格五圈半也能让我跑完花一周时间恢复以及提前一周心理压力爆炸,虽然我那时候跑的不快,但是已经能跑两千米了,瞬间让自己自信心爆炸,并且跑完步出完汗的感觉是非常棒的,毕竟吃奶茶鸡排都能心安理得了,谁叫我跑步了呢😄,其实现在回去看,那时候跑得还算快的,因为还比较瘦,现在要跑得那么快心跳就太快了,关于心跳什么的后面说,开始建立起自信心之后,对跑步这件事就开始不那么排斥跟害怕了,毕竟我能跑两千米了,然后试试三公里,哇,也可以了呢,三公里是什么概念呢,我大学里跑过最多的一次是十二分钟跑五圈半还是六圈,也就是两公里多,不到三公里,几乎是生涯最长了,一时间产生了一些我可能是个被埋没的运动天才的错觉,其实细想下也能明白,只是速度足够慢了就能跑多一点,毕竟提测一千米是要跑进四分钟才及格,自己跑的时候一千米跑六分多钟已经算不慢了(对我自己来说),但是即使是这样还是对把跑步坚持下去这件事有了很大的正面激励作用,并且由于那时候上下班骑车,整个体重控制的比较理想,导致一时间误会跑步就能非常快的减肥(其实这是我跑步历程中比较大的误区之一),因为会在跑步前后称下体重,如果跑个五公里(后面可以跑五公里了),可能体重就能降 0.5 千克,但实际上这只是这五公里跑步身体流失的水分,喝杯水就回来了,那时候能控制体重主要是骑车跟跑步一起的作用,并且工作压力相对来讲比较小,没有过劳肥。
+后面其实跑步慢慢变得一个比较习惯的运动了,从三公里,到五公里,到七公里,再到十公里,十公里差不多对我来说是个坎,一直还不能比较轻松的跑十公里,可能近一两年好了一些(原谅我只是跟自己比较,跟那些大神比差得不知道多远),其实对我来说每次都是个突破,因为其实与他人比较没有特别大意义,比较顶尖的差得太远,比较普通的也不行,都会打击自信心,比较比我差的就更没意义了,所以其实能挑战自己,能把自己的上限提高才是最有意义的,这也是我看着朋友圈里的一些大神的速度除了佩服赞叹之外没什么其他的惭愧或者说嫌弃自己的感觉(阿 Q 精神😄)。
+一直感性地觉得,跑步能解压,跑完浑身汗,有种把身体的负能量都排出去的感觉,也把吃太多的罪恶感排解掉了🤦♂️,之前朋友有看一本书,书名差不多叫越跑越接近自己,这个也是我觉得挺准确的一句话,当跑到接近极限了,还想再继续再跑一点,再跑一点就能突破自己上一次的最远记录了,再跑一点就能又一次突破自己了,成人以后,毕业以后,进入社会以后,世事总是难以件件顺遂,磕磕绊绊的往前走,总觉得要崩溃了,但是还是得坚持,再熬一下,再拼一下,可能还是失败,但人生呢,运气好的人和事总是小概率的,唯有面对挫折,还是日拱一卒,来日方长,我们再坚持下,没准下一次,没准再跑一会,就能突破自己,达到新的境界。
+另外个人后期对跑步的一些知识和理解也变得深入一些,比如伤膝盖,其实跑步的确伤膝盖,需要做一些准备和防护,最好的是适合自己的跑鞋和比较好的路(最好的是塑胶跑道了),也要注意热身跟跑后的拉伸(虽然我做的很差),还有就是注意心率,每个人有自己的适宜心率,我这就不冒充科普达人了,可以自行搜索关键字,先说到这吧~
]]>但是你说这不是个灾难片,而是个反映社会问题的,就业比较容易往这个方向猜,只是剧情会是怎么样的,一时也没啥头绪,后来不知道哪里看了下一个剧情透露,是一个穷人给富人做家教,然后把自己一家都带进富人家,如果是这样的话可能会把这个怎么带进去作为一个主线,不过事实告诉我,这没那么重要,从第一步朋友的介绍,就显得无比顺利,要去当家教了,作为一个穷成这样的人,瞬间转变成一个衣着得体,言行举止都没让富人家看出破绽的延世大学学生,这真的挺难让人理解,所谓江山易改,本性难移,还有就是这人也正好有那么好能力去辅导,并且诡异的是,多惠也是瞬间就喜欢上了男主,多惠跟将男主介绍给她做家教,也就是多惠原来的家教敏赫,应该也有不少的相处时间,这变了有点大了吧,当然这里也可能因为时长需要,如果说这一点是因为时长,那可能我所有的槽点都是因为这个吧,因为我理解的应该是把家里的人如何一步步地带进富人家,这应该是整个剧情的一个需要更多铺垫去克服这个矛盾点,有时候也想过如果我去当导演,是能拍出个啥,没这个机会,可能有也会是很扯淡的,当然这也不能阻拦我谈谈对这个点的一些看法,毕竟评价一台电冰箱不是说我必须得自己会制冷对吧,这大概是我觉得这个电影的第一个槽点,接下去接二连三的,就是我说的这个最核心的矛盾点,不知道谁说过,这种影视剧应该是源自于生活又高于生活,越是好的作品,越要接近生活,这样子才更能有感同身受。
-接下去的点是金基宇介绍金基婷去给多颂当美术家教,这一步又是我理解的败笔吧,就怎么说呢,没什么铺垫,突然从一个社会底层的穷姑娘,转变成一个气场爆表,把富人家太太唬得一愣一愣的,如果说富太太是比较简单无脑的,那富人自己应该是比较有见识而且是做 IT 的,给自己儿子女儿做家教的,查查底细也很正常吧,但是啥都没有,然后呢,她又开始耍司机的心机了,真的是莫名其妙了,司机真的很惨,窈窕淑女君子好逑,而且这个操作也让我摸不着头脑,这是多腹黑并且有经验才会这么操作,脱内裤真的是让我看得一愣愣的,更看得我一愣一愣的,富人竟然也完全按着这个思路去想了,完全没有别的可能呢,甚至可以去查下行车记录仪或者怎样的,或者有没有毛发体液啥的去检验下,毕竟金基婷也乘坐过这辆车,但是最最让我不懂的还是脱内裤这个操作,究竟是什么样的人才会的呢,值得思考。
-金基泽和忠淑的点也是比较奇怪,首先是金基泽,引起最后那个杀人事件的一个由头,大部分观点都是人为朴社长在之前跟老婆啪啪啪的时候说金基泽的身上有股乘地铁的人的味道,简而言之就是穷人的味道,还有去雯光丈夫身下拿钥匙是对金基泽和雯光丈夫身上的味道的鄙夷,可是这个原因真的站不住脚,即使是同样经济水平,如果身上有比较重的异味,背后讨论下,或者闻到了比较重的味道,有不适的表情和动作很正常吧,像雯光丈夫,在地下室里呆了那么久,身上有异味并且比较重太正常了,就跟在厕所呆久了不会觉得味道大,但是从没味道的地方一进有点味道的厕所就会觉得异样,略尴尬的理由;再说忠淑呢,感觉是太厉害了,能胜任这么一家有钱人的各种挑剔的饮食口味要求的保姆职位,也是让人看懵逼了,看到了不禁想到一个问题,这家人开头是那么地穷,不堪,突然转变成这么地像骗子家族,如果有这么好的骗人能力,应该不会到这种地步吧,如果真的是那么穷,没能力,没志气,又怎么会突然变成这么厉害呢,一家人各司其职,把富人家唬得团团转,而这个前提是,这些人的确能胜任这四个位置,这就是我非常不能理解的点。
-然后说回这个标题,寄生虫,不知道是不是翻译过来不准确,如果真的是叫寄生虫的话,这个寄生虫智商未免也太低了,没有像新冠那样机制,致死率低一点,传染能力强一点,潜伏期也能传染,这个寄生虫第一次受到免疫系统的攻击就自爆了;还有呢,作为一个社会比较低层的打工者,乡下人,对这个审题也是不太审的清,是指这一家人是社会的寄生虫,不思进取,并且死的应该,富人是傻白甜,又有钱又善良,这是给有钱人洗地了还是啥,这个奥斯卡真不知道是怎么得的,总觉得奥斯卡,甚至低一点,豆瓣,得奖的,评分高的都是被一群“精英党”把持的,有黑人主角的,得分高;有同性恋的,得分高;结局惨的,得分高;看不懂的,得分高;就像肖申克的救赎,真不知道是哪里好了,最近看了关于明朝那些事的三杨,杨溥的经历应该比这个厉害吧,可是外国人看不懂,就像外国人不懂中国为什么有反分裂国家法,经历了鸦片战争,八国联军,抗日战争等等,其实跟外国对于黑人的权益的问题,因为有南北战争,所以极度重视这个问题,相应的中国也有自己的历史,请理解。
-简而言之我对寄生虫的评分大概 5~6 分吧。
-]]>时光飞逝,我在初中高中的时候因为爱打篮球,以为自己体质已经有了质的变化,所以在体育课跑步的时候妄图跟一位体育非常好的同学一起跑,结果跟的快断气了,最终还是确认了自己是个体育渣,特别是到了大学的第一次体测跑一千米,跑完直接吐了,一则是大学太宅不运动,二则的确是底子不好。那么怎么会去跑步了呢,其实也没什么特殊的原因,就是工作以后因为运动得更少,体质差,而且越来越胖,所以就想运动下,加之跑步也是我觉得成本最低的运动了,刚好那时候17 年租的地方附近小区周围的路车不太多,一圈刚好一公里多,就觉得开始跑跑看,其实想想以前觉得一千米是非常远的,学校塑胶跑道才 400 米,一千米要两圈半,太难了,但是后来在这个小区周围跑的时候好像跑了一圈以后还能再跑一点,最后跑了两圈,可把自己牛坏了,我都能跑两千米了,哈哈,这是个什么概念呢,大学里最让我绝望的两项体育相关的内容就是一千米和十二分钟跑,一千米把我跑吐了,十二分钟跑及格五圈半也能让我跑完花一周时间恢复以及提前一周心理压力爆炸,虽然我那时候跑的不快,但是已经能跑两千米了,瞬间让自己自信心爆炸,并且跑完步出完汗的感觉是非常棒的,毕竟吃奶茶鸡排都能心安理得了,谁叫我跑步了呢😄,其实现在回去看,那时候跑得还算快的,因为还比较瘦,现在要跑得那么快心跳就太快了,关于心跳什么的后面说,开始建立起自信心之后,对跑步这件事就开始不那么排斥跟害怕了,毕竟我能跑两千米了,然后试试三公里,哇,也可以了呢,三公里是什么概念呢,我大学里跑过最多的一次是十二分钟跑五圈半还是六圈,也就是两公里多,不到三公里,几乎是生涯最长了,一时间产生了一些我可能是个被埋没的运动天才的错觉,其实细想下也能明白,只是速度足够慢了就能跑多一点,毕竟提测一千米是要跑进四分钟才及格,自己跑的时候一千米跑六分多钟已经算不慢了(对我自己来说),但是即使是这样还是对把跑步坚持下去这件事有了很大的正面激励作用,并且由于那时候上下班骑车,整个体重控制的比较理想,导致一时间误会跑步就能非常快的减肥(其实这是我跑步历程中比较大的误区之一),因为会在跑步前后称下体重,如果跑个五公里(后面可以跑五公里了),可能体重就能降 0.5 千克,但实际上这只是这五公里跑步身体流失的水分,喝杯水就回来了,那时候能控制体重主要是骑车跟跑步一起的作用,并且工作压力相对来讲比较小,没有过劳肥。
-后面其实跑步慢慢变得一个比较习惯的运动了,从三公里,到五公里,到七公里,再到十公里,十公里差不多对我来说是个坎,一直还不能比较轻松的跑十公里,可能近一两年好了一些(原谅我只是跟自己比较,跟那些大神比差得不知道多远),其实对我来说每次都是个突破,因为其实与他人比较没有特别大意义,比较顶尖的差得太远,比较普通的也不行,都会打击自信心,比较比我差的就更没意义了,所以其实能挑战自己,能把自己的上限提高才是最有意义的,这也是我看着朋友圈里的一些大神的速度除了佩服赞叹之外没什么其他的惭愧或者说嫌弃自己的感觉(阿 Q 精神😄)。
-一直感性地觉得,跑步能解压,跑完浑身汗,有种把身体的负能量都排出去的感觉,也把吃太多的罪恶感排解掉了🤦♂️,之前朋友有看一本书,书名差不多叫越跑越接近自己,这个也是我觉得挺准确的一句话,当跑到接近极限了,还想再继续再跑一点,再跑一点就能突破自己上一次的最远记录了,再跑一点就能又一次突破自己了,成人以后,毕业以后,进入社会以后,世事总是难以件件顺遂,磕磕绊绊的往前走,总觉得要崩溃了,但是还是得坚持,再熬一下,再拼一下,可能还是失败,但人生呢,运气好的人和事总是小概率的,唯有面对挫折,还是日拱一卒,来日方长,我们再坚持下,没准下一次,没准再跑一会,就能突破自己,达到新的境界。
-另外个人后期对跑步的一些知识和理解也变得深入一些,比如伤膝盖,其实跑步的确伤膝盖,需要做一些准备和防护,最好的是适合自己的跑鞋和比较好的路(最好的是塑胶跑道了),也要注意热身跟跑后的拉伸(虽然我做的很差),还有就是注意心率,每个人有自己的适宜心率,我这就不冒充科普达人了,可以自行搜索关键字,先说到这吧~
-]]>最近这次经历也是有火绒的一定责任,在我尝试推出 U盘的时候提示了我被另一个大流氓软件,XlibabaProtect.exe 占用了,这个流氓软件真的是充分展示了某里的技术实力,试过 N 多种办法都关不掉也删不掉,尝试了很多种办法也没办法删除,但是后面换了种思路,一般这种情况肯定是有进程在占用 U盘里的内容,最新版本的 Powertoys 会在文件的右键菜单里添加一个叫 File Locksmith 的功能,可以用于检查正在使用哪些文件以及由哪些进程使用,但是可能是我的使用姿势不对,没有仔细看文档,它里面有个”以管理员身份重启”,可能会有用。
这算是第一种方式,
第二种方式是 Windows 任务管理器中性能 tab 下的”打开资源监视器”,
,假如我的 U 盘的盘符是F:
就可以搜索到占用这个盘符下文件的进程,这里千万小心‼️‼️,不可轻易杀掉这些进程,有些系统进程如果轻易杀掉会导致蓝屏等问题,不可轻易尝试,除非能确认这些进程的作用。
对于前两种方式对我来说都无效,
所以尝试了第三种,
就是磁盘脱机的方式,在”计算机”右键管理,点击”磁盘管理”,可以找到 U 盘盘符右键,点击”脱机”,然后再”推出”,这个对我来说也不行
这种是唯一对我有效的,在开始菜单搜索”event”,可以搜到”事件查看器”,
,这个可以看到当前最近 Windows 发生的事件,打开这个后就点击U盘推出,因为推不出来也是一种错误事件,点击下刷新就能在这看到具体是因为什么推出不了,具体的进程信息
最后发现是英特尔的驱动管理程序的一个进程,关掉就退出了,虽然前面说的某里的进程是流氓,但这边是真的冤枉它了
Key differences
utf8mb4_unicode_ci is based on the official Unicode rules for universal sorting and comparison, which sorts accurately in a wide range of languages.
utf8mb4_general_ci is a simplified set of sorting rules which aims to do as well as it can while taking many short-cuts designed to improve speed. It does not follow the Unicode rules and will result in undesirable sorting or comparison in some situations, such as when using particular languages or characters.
On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.
-Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci
utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.
-As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:
For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).
Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.
In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.
What should you use?
-There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.
In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.
There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.
One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.
-What the parts mean
-Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.
-Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.
-And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.
-对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci。
所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。
-_unicode_ci和 _general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci或 _general_ci。下面对那些较旧的排序规则的描述仅供参考。
MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。
主要区别
-utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。
utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。
在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。
-utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势
utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。
就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:
例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。
一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。
在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。
你应该用什么?
-几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。
过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。
有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。
我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。
-其余各个部分是什么意思
-首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。
接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。
-最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。
-UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.
-So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.
-This is what (a previous version of the same page at)the MySQL documentationhas to say about it:
---The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:
--
-- For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
-- For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
-
So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.
-UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。
-所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较。
-这就是(同一页面的先前版本)MySQL 文档 不得不说的:
---名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:
--
-- 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
-- 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
-
因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅
-实际使用中最常见的非 BMP Unicode 字符是什么? 。
-]]> -public static void main(String[] args) throws InterruptedException, MQClientException {
-
- /*
- * Instantiate with specified consumer group name.
- * 首先是new 一个对象出来,然后指定 Consumer 的 Group
- * 同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
- */
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
-
- /*
- * Specify name server addresses.
- * <p/>
- * 这里可以通知指定环境变量或者设置对象参数的形式指定名字空间服务的地址
- *
- * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
- * <pre>
- * {@code
- * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
- * }
- * </pre>
- */
-
- /*
- * Specify where to start in case the specified consumer group is a brand new one.
- * 指定消费起始点
- */
- consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
-
- /*
- * Subscribe one more more topics to consume.
- * 指定订阅的 topic 跟 tag,注意后面的是个表达式,可以以 tag1 || tag2 || tag3 传入
- */
- consumer.subscribe("TopicTest", "*");
-
- /*
- * Register callback to execute on arrival of messages fetched from brokers.
- * 注册具体获得消息后的处理方法
- */
- consumer.registerMessageListener(new MessageListenerConcurrently() {
-
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
- ConsumeConcurrentlyContext context) {
- System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- });
-
- /*
- * Launch the consumer instance.
- * 启动消费者
- */
- consumer.start();
-
- System.out.printf("Consumer Started.%n");
- }
-
-然后就是看看 start 的过程了
-/**
- * This method gets internal infrastructure readily to serve. Instances must call this method after configuration.
- *
- * @throws MQClientException if there is any client error.
- */
- @Override
- public void start() throws MQClientException {
- setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
- this.defaultMQPushConsumerImpl.start();
- if (null != traceDispatcher) {
- try {
- traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
- } catch (MQClientException e) {
- log.warn("trace dispatcher start failed ", e);
- }
- }
- }
-具体的逻辑在this.defaultMQPushConsumerImpl.start(),这个 defaultMQPushConsumerImpl 就是
/**
- * Internal implementation. Most of the functions herein are delegated to it.
- */
- protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
-
-public synchronized void start() throws MQClientException {
- switch (this.serviceState) {
- case CREATE_JUST:
- log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
- this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
- // 这里比较巧妙,相当于想设立了个屏障,防止并发启动,不过这里并不是悲观锁,也不算个严格的乐观锁
- this.serviceState = ServiceState.START_FAILED;
-
- this.checkConfig();
-
- this.copySubscription();
-
- if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
- this.defaultMQPushConsumer.changeInstanceNameToPID();
- }
-
- // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成
- this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
-
- // 这个 rebalanceImpl 主要负责决定,当前的consumer应该从哪些Queue中消费消息;
- this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
- this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
- this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
- this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
-
- // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑
- this.pullAPIWrapper = new PullAPIWrapper(
- mQClientFactory,
- this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
- this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
-
- if (this.defaultMQPushConsumer.getOffsetStore() != null) {
- this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
- } else {
- switch (this.defaultMQPushConsumer.getMessageModel()) {
- case BROADCASTING:
- this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
- break;
- case CLUSTERING:
- this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
- break;
- default:
- break;
- }
- this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
- }
- // offsetStore 维护当前consumer的消费记录(offset);有两种实现,Local和Rmote,Local存储在本地磁盘上,适用于BROADCASTING广播消费模式;而Remote则将消费进度存储在Broker上,适用于CLUSTERING集群消费模式;
- this.offsetStore.load();
-
- if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
- this.consumeOrderly = true;
- this.consumeMessageService =
- new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
- } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
- this.consumeOrderly = false;
- this.consumeMessageService =
- new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
- }
-
- // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
- this.consumeMessageService.start();
-
- boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
- if (!registerOK) {
- this.serviceState = ServiceState.CREATE_JUST;
- this.consumeMessageService.shutdown();
- throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
- + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
- null);
- }
-
- mQClientFactory.start();
- log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
- this.serviceState = ServiceState.RUNNING;
- break;
- case RUNNING:
- case START_FAILED:
- case SHUTDOWN_ALREADY:
- throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
- + this.serviceState
- + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
- null);
- default:
- break;
- }
-
- this.updateTopicSubscribeInfoWhenSubscriptionChanged();
- this.mQClientFactory.checkClientInBroker();
- this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
- this.mQClientFactory.rebalanceImmediately();
- }
-然后我们往下看主要的目光聚焦mQClientFactory.start()
public void start() throws MQClientException {
-
- synchronized (this) {
- switch (this.serviceState) {
- case CREATE_JUST:
- this.serviceState = ServiceState.START_FAILED;
- // If not specified,looking address from name server
- if (null == this.clientConfig.getNamesrvAddr()) {
- this.mQClientAPIImpl.fetchNameServerAddr();
- }
- // Start request-response channel
- // 这里主要是初始化了个网络客户端
- this.mQClientAPIImpl.start();
- // Start various schedule tasks
- // 定时任务
- this.startScheduledTask();
- // Start pull service
- // 这里重点说下
- this.pullMessageService.start();
- // Start rebalance service
- this.rebalanceService.start();
- // Start push service
- this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
- log.info("the client factory [{}] start OK", this.clientId);
- this.serviceState = ServiceState.RUNNING;
- break;
- case START_FAILED:
- throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
- default:
- break;
- }
- }
- }
-我们来看下这个 pullMessageService,org.apache.rocketmq.client.impl.consumer.PullMessageService,
实现了 runnable 接口,
然后可以看到 run 方法
public void run() {
- log.info(this.getServiceName() + " service started");
-
- while (!this.isStopped()) {
- try {
- PullRequest pullRequest = this.pullRequestQueue.take();
- this.pullMessage(pullRequest);
- } catch (InterruptedException ignored) {
- } catch (Exception e) {
- log.error("Pull Message Service Run Method exception", e);
- }
- }
-
- log.info(this.getServiceName() + " service end");
- }
-接着在看 pullMessage 方法
-private void pullMessage(final PullRequest pullRequest) {
- final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
- if (consumer != null) {
- DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
- impl.pullMessage(pullRequest);
- } else {
- log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
- }
- }
-实际上调用了这个方法,这个方法很长,我在代码里注释下下每一段的功能
-public void pullMessage(final PullRequest pullRequest) {
- final ProcessQueue processQueue = pullRequest.getProcessQueue();
- // 这里开始就是检查状态,确定是否往下执行
- if (processQueue.isDropped()) {
- log.info("the pull request[{}] is dropped.", pullRequest.toString());
- return;
- }
-
- pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
-
- try {
- this.makeSureStateOK();
- } catch (MQClientException e) {
- log.warn("pullMessage exception, consumer state not ok", e);
- this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
- return;
- }
-
- if (this.isPause()) {
- log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
- this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
- return;
- }
-
- // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制
- long cachedMessageCount = processQueue.getMsgCount().get();
- long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
-
- if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
- this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
- if ((queueFlowControlTimes++ % 1000) == 0) {
- log.warn(
- "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
- this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
- }
- return;
- }
-
- if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
- this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
- if ((queueFlowControlTimes++ % 1000) == 0) {
- log.warn(
- "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
- this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
- }
- return;
- }
-
- // 若不是顺序消费(即DefaultMQPushConsumerImpl.consumeOrderly等于false),则检查ProcessQueue对象的msgTreeMap:TreeMap<Long,MessageExt>变量的第一个key值与最后一个key值之间的差额,该key值表示查询的队列偏移量queueoffset;若差额大于阈值(由DefaultMQPushConsumer. consumeConcurrentlyMaxSpan指定,默认是2000),则调用PullMessageService.executePullRequestLater方法,在50毫秒之后重新将该PullRequest请求放入PullMessageService.pullRequestQueue队列中;并跳出该方法;这里的意思主要就是消息有堆积了,等会再来拉取
- if (!this.consumeOrderly) {
- if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
- this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
- if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
- log.warn(
- "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
- processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
- pullRequest, queueMaxSpanFlowControlTimes);
- }
- return;
- }
- } else {
- if (processQueue.isLocked()) {
- if (!pullRequest.isLockedFirst()) {
- final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
- boolean brokerBusy = offset < pullRequest.getNextOffset();
- log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
- pullRequest, offset, brokerBusy);
- if (brokerBusy) {
- log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
- pullRequest, offset);
- }
-
- pullRequest.setLockedFirst(true);
- pullRequest.setNextOffset(offset);
- }
- } else {
- this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
- log.info("pull message later because not locked in broker, {}", pullRequest);
- return;
- }
- }
-
- // 以PullRequest.messageQueue对象的topic值为参数从RebalanceImpl.subscriptionInner: ConcurrentHashMap, SubscriptionData>中获取对应的SubscriptionData对象,若该对象为null,考虑到并发的关系,调用executePullRequestLater方法,稍后重试;并跳出该方法;
- final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
- if (null == subscriptionData) {
- this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
- log.warn("find the consumer's subscription failed, {}", pullRequest);
- return;
- }
-
- final long beginTimestamp = System.currentTimeMillis();
-
- // 异步拉取回调,先不讨论细节
- PullCallback pullCallback = new PullCallback() {
- @Override
- public void onSuccess(PullResult pullResult) {
- if (pullResult != null) {
- pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
- subscriptionData);
-
- switch (pullResult.getPullStatus()) {
- case FOUND:
- long prevRequestOffset = pullRequest.getNextOffset();
- pullRequest.setNextOffset(pullResult.getNextBeginOffset());
- long pullRT = System.currentTimeMillis() - beginTimestamp;
- DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
- pullRequest.getMessageQueue().getTopic(), pullRT);
-
- long firstMsgOffset = Long.MAX_VALUE;
- if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
- DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
- } else {
- firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
-
- DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
- pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
-
- boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
- DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
- pullResult.getMsgFoundList(),
- processQueue,
- pullRequest.getMessageQueue(),
- dispatchToConsume);
-
- if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
- DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
- DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
- } else {
- DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
- }
- }
-
- if (pullResult.getNextBeginOffset() < prevRequestOffset
- || firstMsgOffset < prevRequestOffset) {
- log.warn(
- "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
- pullResult.getNextBeginOffset(),
- firstMsgOffset,
- prevRequestOffset);
- }
-
- break;
- case NO_NEW_MSG:
- pullRequest.setNextOffset(pullResult.getNextBeginOffset());
-
- DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
-
- DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
- break;
- case NO_MATCHED_MSG:
- pullRequest.setNextOffset(pullResult.getNextBeginOffset());
-
- DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
-
- DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
- break;
- case OFFSET_ILLEGAL:
- log.warn("the pull request offset illegal, {} {}",
- pullRequest.toString(), pullResult.toString());
- pullRequest.setNextOffset(pullResult.getNextBeginOffset());
-
- pullRequest.getProcessQueue().setDropped(true);
- DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
-
- @Override
- public void run() {
- try {
- DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
- pullRequest.getNextOffset(), false);
-
- DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
-
- DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
-
- log.warn("fix the pull request offset, {}", pullRequest);
- } catch (Throwable e) {
- log.error("executeTaskLater Exception", e);
- }
- }
- }, 10000);
- break;
- default:
- break;
- }
- }
- }
-
- @Override
- public void onException(Throwable e) {
- if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
- log.warn("execute the pull request exception", e);
- }
-
- DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
- }
- };
- // 如果为集群模式,即可置commitOffsetEnable为 true
- boolean commitOffsetEnable = false;
- long commitOffsetValue = 0L;
- if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
- commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
- if (commitOffsetValue > 0) {
- commitOffsetEnable = true;
- }
- }
-
- // 将上面获得的commitOffsetEnable更新到订阅关系里
- String subExpression = null;
- boolean classFilter = false;
- SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
- if (sd != null) {
- if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) {
- subExpression = sd.getSubString();
- }
-
- classFilter = sd.isClassFilterMode();
- }
-
- // 组成 sysFlag
- int sysFlag = PullSysFlag.buildSysFlag(
- commitOffsetEnable, // commitOffset
- true, // suspend
- subExpression != null, // subscription
- classFilter // class filter
- );
- // 调用真正的拉取消息接口
- try {
- this.pullAPIWrapper.pullKernelImpl(
- pullRequest.getMessageQueue(),
- subExpression,
- subscriptionData.getExpressionType(),
- subscriptionData.getSubVersion(),
- pullRequest.getNextOffset(),
- this.defaultMQPushConsumer.getPullBatchSize(),
- sysFlag,
- commitOffsetValue,
- BROKER_SUSPEND_MAX_TIME_MILLIS,
- CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
- CommunicationMode.ASYNC,
- pullCallback
- );
- } catch (Exception e) {
- log.error("pullKernelImpl exception", e);
- this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
- }
- }
-以下就是拉取消息的底层 api,不够不是特别复杂,主要是在找 broker,和设置请求参数
-public PullResult pullKernelImpl(
- final MessageQueue mq,
- final String subExpression,
- final String expressionType,
- final long subVersion,
- final long offset,
- final int maxNums,
- final int sysFlag,
- final long commitOffset,
- final long brokerSuspendMaxTimeMillis,
- final long timeoutMillis,
- final CommunicationMode communicationMode,
- final PullCallback pullCallback
-) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
- FindBrokerResult findBrokerResult =
- this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
- this.recalculatePullFromWhichNode(mq), false);
- if (null == findBrokerResult) {
- this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
- findBrokerResult =
- this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
- this.recalculatePullFromWhichNode(mq), false);
- }
-
- if (findBrokerResult != null) {
- {
- // check version
- if (!ExpressionType.isTagType(expressionType)
- && findBrokerResult.getBrokerVersion() < MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) {
- throw new MQClientException("The broker[" + mq.getBrokerName() + ", "
- + findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null);
- }
- }
- int sysFlagInner = sysFlag;
-
- if (findBrokerResult.isSlave()) {
- sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
- }
-
- PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
- requestHeader.setConsumerGroup(this.consumerGroup);
- requestHeader.setTopic(mq.getTopic());
- requestHeader.setQueueId(mq.getQueueId());
- requestHeader.setQueueOffset(offset);
- requestHeader.setMaxMsgNums(maxNums);
- requestHeader.setSysFlag(sysFlagInner);
- requestHeader.setCommitOffset(commitOffset);
- requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);
- requestHeader.setSubscription(subExpression);
- requestHeader.setSubVersion(subVersion);
- requestHeader.setExpressionType(expressionType);
-
- String brokerAddr = findBrokerResult.getBrokerAddr();
- if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
- brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
- }
-
- PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
- brokerAddr,
- requestHeader,
- timeoutMillis,
- communicationMode,
- pullCallback);
-
- return pullResult;
- }
-
- throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
-}
-再看下一步的
-public PullResult pullMessage(
- final String addr,
- final PullMessageRequestHeader requestHeader,
- final long timeoutMillis,
- final CommunicationMode communicationMode,
- final PullCallback pullCallback
-) throws RemotingException, MQBrokerException, InterruptedException {
- RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);
-
- switch (communicationMode) {
- case ONEWAY:
- assert false;
- return null;
- case ASYNC:
- this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
- return null;
- case SYNC:
- return this.pullMessageSync(addr, request, timeoutMillis);
- default:
- assert false;
- break;
- }
-
- return null;
-}
-通过 communicationMode 判断是同步拉取还是异步拉取,异步就调用
-private void pullMessageAsync(
- final String addr,
- final RemotingCommand request,
- final long timeoutMillis,
- final PullCallback pullCallback
- ) throws RemotingException, InterruptedException {
- this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
- @Override
- public void operationComplete(ResponseFuture responseFuture) {
- 异步
- RemotingCommand response = responseFuture.getResponseCommand();
- if (response != null) {
- try {
- PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response);
- assert pullResult != null;
- pullCallback.onSuccess(pullResult);
- } catch (Exception e) {
- pullCallback.onException(e);
- }
- } else {
- if (!responseFuture.isSendRequestOK()) {
- pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause()));
- } else if (responseFuture.isTimeout()) {
- pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request,
- responseFuture.getCause()));
- } else {
- pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause()));
- }
- }
- }
- });
- }
-并且会调用前面 pullCallback 的onSuccess和onException方法,同步的就是调用
-private PullResult pullMessageSync(
- final String addr,
- final RemotingCommand request,
- final long timeoutMillis
- ) throws RemotingException, InterruptedException, MQBrokerException {
- RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
- assert response != null;
- return this.processPullResponse(response);
- }
-然后就是这个 remotingClient 的 invokeAsync 跟 invokeSync 方法
-@Override
- public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
- throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
- RemotingSendRequestException {
- long beginStartTime = System.currentTimeMillis();
- final Channel channel = this.getAndCreateChannel(addr);
- if (channel != null && channel.isActive()) {
- try {
- doBeforeRpcHooks(addr, request);
- long costTime = System.currentTimeMillis() - beginStartTime;
- if (timeoutMillis < costTime) {
- throw new RemotingTooMuchRequestException("invokeAsync call timeout");
- }
- this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, invokeCallback);
- } catch (RemotingSendRequestException e) {
- log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
- this.closeChannel(addr, channel);
- throw e;
- }
- } else {
- this.closeChannel(addr, channel);
- throw new RemotingConnectException(addr);
- }
- }
-@Override
- public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
- throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
- long beginStartTime = System.currentTimeMillis();
- final Channel channel = this.getAndCreateChannel(addr);
- if (channel != null && channel.isActive()) {
- try {
- doBeforeRpcHooks(addr, request);
- long costTime = System.currentTimeMillis() - beginStartTime;
- if (timeoutMillis < costTime) {
- throw new RemotingTimeoutException("invokeSync call timeout");
- }
- RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
- doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
- return response;
- } catch (RemotingSendRequestException e) {
- log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
- this.closeChannel(addr, channel);
- throw e;
- } catch (RemotingTimeoutException e) {
- if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
- this.closeChannel(addr, channel);
- log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
- }
- log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
- throw e;
- }
- } else {
- this.closeChannel(addr, channel);
- throw new RemotingConnectException(addr);
- }
- }
-再往下看
-public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
- final long timeoutMillis)
- throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
- final int opaque = request.getOpaque();
-
- try {
- 同步跟异步都是会把结果用ResponseFuture抱起来
- final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
- this.responseTable.put(opaque, responseFuture);
- final SocketAddress addr = channel.remoteAddress();
- channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
- @Override
- public void operationComplete(ChannelFuture f) throws Exception {
- if (f.isSuccess()) {
- responseFuture.setSendRequestOK(true);
- return;
- } else {
- responseFuture.setSendRequestOK(false);
- }
-
- responseTable.remove(opaque);
- responseFuture.setCause(f.cause());
- responseFuture.putResponse(null);
- log.warn("send a request command to channel <" + addr + "> failed.");
- }
- });
- // 区别是同步的是在这等待
- RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
- if (null == responseCommand) {
- if (responseFuture.isSendRequestOK()) {
- throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
- responseFuture.getCause());
- } else {
- throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
- }
- }
-
- return responseCommand;
- } finally {
- this.responseTable.remove(opaque);
- }
- }
-
- public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
- final InvokeCallback invokeCallback)
- throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
- long beginStartTime = System.currentTimeMillis();
- final int opaque = request.getOpaque();
- boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
- if (acquired) {
- final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
- long costTime = System.currentTimeMillis() - beginStartTime;
- if (timeoutMillis < costTime) {
- once.release();
- throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
- }
-
- final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
- this.responseTable.put(opaque, responseFuture);
- try {
- channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
- @Override
- public void operationComplete(ChannelFuture f) throws Exception {
- if (f.isSuccess()) {
- responseFuture.setSendRequestOK(true);
- return;
- }
- requestFail(opaque);
- log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
- }
- });
- } catch (Exception e) {
- responseFuture.release();
- log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
- throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
- }
- } else {
- if (timeoutMillis <= 0) {
- throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
- } else {
- String info =
- String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
- timeoutMillis,
- this.semaphoreAsync.getQueueLength(),
- this.semaphoreAsync.availablePermits()
- );
- log.warn(info);
- throw new RemotingTimeoutException(info);
- }
- }
- }
-
-
-
+On modern servers, this performance boost will be all but negligible. It was devised in a time when servers had a tiny fraction of the CPU performance of today’s computers.
+Benefits of utf8mb4_unicode_ci over utf8mb4_general_ci
utf8mb4_unicode_ci, which uses the Unicode rules for sorting and comparison, employs a fairly complex algorithm for correct sorting in a wide range of languages and when using a wide range of special characters. These rules need to take into account language-specific conventions; not everybody sorts their characters in what we would call ‘alphabetical order’.
+As far as Latin (ie “European”) languages go, there is not much difference between the Unicode sorting and the simplified utf8mb4_general_cisorting in MySQL, but there are still a few differences:
For examples, the Unicode collation sorts “ß” like “ss”, and “Œ” like “OE” as people using those characters would normally want, whereas utf8mb4_general_cisorts them as single characters (presumably like “s” and “e” respectively).
Some Unicode characters are defined as ignorable, which means they shouldn’t count toward the sort order and the comparison should move on to the next character instead. utf8mb4_unicode_cihandles these properly.
In non-latin languages, such as Asian languages or languages with different alphabets, there may be a lot more differences between Unicode sorting and the simplified utf8mb4_general_cisorting. The suitability of utf8mb4_general_ciwill depend heavily on the language used. For some languages, it’ll be quite inadequate.
What should you use?
+There is almost certainly no reason to use utf8mb4_general_cianymore, as we have left behind the point where CPU speed is low enough that the performance difference would be important. Your database will almost certainly be limited by other bottlenecks than this.
In the past, some people recommended to use utf8mb4_general_ciexcept when accurate sorting was going to be important enough to justify the performance cost. Today, that performance cost has all but disappeared, and developers are treating internationalization more seriously.
There’s an argument to be made that if speed is more important to you than accuracy, you may as well not do any sorting at all. It’s trivial to make an algorithm faster if you do not need it to be accurate. So, utf8mb4_general_ciis a compromise that’s probably not needed for speed reasons and probably also not suitable for accuracy reasons.
One other thing I’ll add is that even if you know your application only supports the English language, it may still need to deal with people’s names, which can often contain characters used in other languages in which it is just as important to sort correctly. Using the Unicode rules for everything helps add peace of mind that the very smart Unicode people have worked very hard to make sorting work properly.
+What the parts mean
+Firstly, ci is for case-insensitive sorting and comparison. This means it’s suitable for textual data, and case is not important. The other types of collation are cs (case-sensitive) for textual data where case is important, and bin, for where the encoding needs to match, bit for bit, which is suitable for fields which are really encoded binary data (including, for example, Base64). Case-sensitive sorting leads to some weird results and case-sensitive comparison can result in duplicate values differing only in letter case, so case-sensitive collations are falling out of favor for textual data - if case is significant to you, then otherwise ignorable punctuation and so on is probably also significant, and a binary collation might be more appropriate.
+Next, unicode or general refers to the specific sorting and comparison rules - in particular, the way text is normalized or compared. There are many different sets of rules for the utf8mb4 character encoding, with unicode and general being two that attempt to work well in all possible languages rather than one specific one. The differences between these two sets of rules are the subject of this answer. Note that unicode uses rules from Unicode 4.0. Recent versions of MySQL add the rulesets unicode_520 using rules from Unicode 5.2, and 0900 (dropping the “unicode_” part) using rules from Unicode 9.0.
+And lastly, utf8mb4 is of course the character encoding used internally. In this answer I’m talking only about Unicode based encodings.
+对于那些在 2020 年或之后仍会遇到这个问题的人,有可能比这两个更好的新选项。例如,utf8mb4_0900_ai_ci。
所有这些排序规则都用于 UTF-8 字符编码。不同之处在于文本的排序和比较方式。
+_unicode_ci和 _general_ci是两组不同的规则,用于按照我们期望的方式对文本进行排序和比较。较新版本的 MySQL 也引入了新的规则集,例如 _0900_ai_ci用于基于 Unicode 9.0 的等效规则 - 并且没有等效的 _general_ci变体。现在阅读本文的人可能应该使用这些较新的排序规则之一,而不是 _unicode_ci或 _general_ci。下面对那些较旧的排序规则的描述仅供参考。
MySQL 目前正在从旧的、有缺陷的 UTF-8 实现过渡。现在,您需要使用 utf8mb4 而不是 utf8作为字符编码部分,以确保您获得的是固定版本。有缺陷的版本仍然是为了向后兼容,尽管它已被弃用。
主要区别
+utf8mb4_unicode_ci基于官方 Unicode 规则进行通用排序和比较,可在多种语言中准确排序。
utf8mb4_general_ci是一组简化的排序规则,旨在尽其所能,同时采用许多旨在提高速度的捷径。它不遵循 Unicode 规则,并且在某些情况下会导致不希望的排序或比较,例如在使用特定语言或字符时。
在现代服务器上,这种性能提升几乎可以忽略不计。它是在服务器的 CPU 性能只有当今计算机的一小部分时设计的。
+utf8mb4_unicode_ci 相对于 utf8mb4_general_ci的优势
utf8mb4_unicode_ci使用 Unicode 规则进行排序和比较,采用相当复杂的算法在多种语言中以及在使用多种特殊字符时进行正确排序。这些规则需要考虑特定语言的约定;不是每个人都按照我们所说的“字母顺序”对他们的字符进行排序。
就拉丁语(即“欧洲”)语言而言,Unicode 排序和 MySQL 中简化的 utf8mb4_general_ci排序没有太大区别,但仍有一些区别:
例如,Unicode 排序规则将“ß”排序为“ss”,将“Œ”排序为“OE”,因为使用这些字符的人通常需要这些字符,而 utf8mb4_general_ci将它们排序为单个字符(大概分别像“s”和“e” )。
一些 Unicode 字符被定义为可忽略,这意味着它们不应该计入排序顺序,并且比较应该转到下一个字符。 utf8mb4_unicode_ci正确处理这些。
在非拉丁语言中,例如亚洲语言或具有不同字母的语言,Unicode 排序和简化的 utf8mb4_general_ci排序之间可能存在更多差异。 utf8mb4_general_ci的适用性在很大程度上取决于所使用的语言。对于某些语言,这将是非常不充分的。
你应该用什么?
+几乎可以肯定没有理由再使用 utf8mb4_general_ci,因为我们已经将 CPU 速度低到会严重影响性能表现的时代远抛在脑后了。您的数据库几乎肯定会受到除此之外的其他瓶颈的限制。
过去,有些人建议使用 utf8mb4_general_ci,除非准确排序足够重要以证明性能成本是合理的。如今,这种性能成本几乎消失了,开发人员正在更加认真地对待国际化。
有一个论点是,如果速度对您来说比准确性更重要,那么您可能根本不进行任何排序。如果您不需要准确的算法,那么使算法更快是微不足道的。因此,utf8mb4_general_ci是一种折衷方案,出于速度原因可能不需要,也可能出于准确性原因也不适合。
我要补充的另一件事是,即使您知道您的应用程序仅支持英语,它可能仍需要处理人名,这些人名通常包含其他语言中使用的字符,在这些语言中正确排序同样重要.对所有事情都使用 Unicode 规则有助于让您更加安心,因为非常聪明的 Unicode 人员已经非常努力地工作以使排序正常工作。
+其余各个部分是什么意思
+首先, ci 用于不区分大小写的排序和比较。这意味着它适用于文本数据,大小写并不重要。其他类型的排序规则是 cs(区分大小写),用于区分大小写的文本数据,以及 bin,用于编码需要匹配的地方,逐位匹配,适用于真正编码二进制数据的字段(包括,用于例如,Base64)。区分大小写的排序会导致一些奇怪的结果,区分大小写的比较可能会导致重复值仅在字母大小写上有所不同,因此区分大小写的排序规则对文本数据不受欢迎 - 如果大小写对您很重要,那么标点符号就可以忽略等等可能也很重要,二进制排序规则可能更合适。
接下来,unicode 或general 指的是具体的排序和比较规则——特别是文本被规范化或比较的方式。 utf8mb4 字符编码有许多不同的规则集,其中 unicode 和 general 是两种,它们试图在所有可能的语言中都很好地工作,而不是在一种特定的语言中。这两组规则之间的差异是此答案的主题。请注意,unicode 使用 Unicode 4.0 中的规则。 MySQL 的最新版本使用 Unicode 5.2 的规则添加规则集 unicode_520,使用 Unicode 9.0 的规则添加 0900(删除“unicode_”部分)。
+最后,utf8mb4 当然是内部使用的字符编码。在这个答案中,我只谈论基于 Unicode 的编码。
+UTF-8is a variable-length encoding. In the case of UTF-8, this means that storing one code point requires one to four bytes. However, MySQL’s encoding called “utf8” (alias of “utf8mb3”) only stores a maximum of three bytes per code point.
+So the character set “utf8”/“utf8mb3” cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the “Basic Multilingual Plane“. See also Comparison of Unicode encodings.
+This is what (a previous version of the same page at)the MySQL documentationhas to say about it:
+++The character set named utf8[/utf8mb3] uses a maximum of three bytes per character and contains only BMP characters. As of MySQL 5.5.3, the utf8mb4 character set uses a maximum of four bytes per character supports supplemental characters:
++
+- For a BMP character, utf8[/utf8mb3] and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.
+- For a supplementary character, utf8[/utf8mb3] cannot store the character at all, while utf8mb4 requires four bytes to store it. Since utf8[/utf8mb3] cannot store the character at all, you do not have any supplementary characters in utf8[/utf8mb3] columns and you need not worry about converting characters or losing data when upgrading utf8[/utf8mb3] data from older versions of MySQL.
+
So if you want your column to support storing characters lying outside the BMP (and you usually want to), such as emoji, use “utf8mb4”. See also What are the most common non-BMP Unicode characters in actual use?.
+UTF-8 是一种可变长度编码。对于 UTF-8,这意味着存储一个代码点需要一到四个字节。但是,MySQL 的编码称为“utf8”(“utf8mb3”的别名)每个代码点最多只能存储三个字节。
+所以字符集“utf8”/“utf8mb3”不能存储所有的Unicode码位:它只支持0x000到0xFFFF的范围,被称为“基本多语言平面”。另请参阅 Unicode 编码比较。
+这就是(同一页面的先前版本)MySQL 文档 不得不说的:
+++名为 utf8[/utf8mb3] 的字符集每个字符最多使用三个字节,并且仅包含 BMP 字符。从 MySQL 5.5.3 开始,utf8mb4 字符集每个字符最多使用四个字节,支持补充字符:
++
+- 对于 BMP 字符,utf8[/utf8mb3] 和 utf8mb4 具有相同的存储特性:相同的代码值、相同的编码、相同的长度。
+- 对于补充字符,utf8[/utf8mb3] 根本无法存储该字符,而 utf8mb4 需要四个字节来存储它。由于 utf8[/utf8mb3] 根本无法存储字符,因此您在 utf8[/utf8mb3] 列中没有任何补充字符,您不必担心从旧版本升级 utf8[/utf8mb3] 数据时转换字符或丢失数据mysql。
+
因此,如果您希望您的列支持存储位于 BMP 之外的字符(并且您通常希望这样做),例如 emoji,请使用“utf8mb4”。另请参阅
+ ]]>public static void main(String[] args) {
- main0(args);
- }
-
- public static NamesrvController main0(String[] args) {
-
- try {
- NamesrvController controller = createNamesrvController(args);
- start(controller);
- String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
- log.info(tip);
- System.out.printf("%s%n", tip);
- return controller;
- } catch (Throwable e) {
- e.printStackTrace();
- System.exit(-1);
- }
-
- return null;
- }
-
-入口的代码时这样子,其实主要的逻辑在createNamesrvController和start方法,来看下这两个的实现
-public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
- System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
- //PackageConflictDetect.detectFastjson();
-
- Options options = ServerUtil.buildCommandlineOptions(new Options());
- commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
- if (null == commandLine) {
- System.exit(-1);
- return null;
- }
-
- final NamesrvConfig namesrvConfig = new NamesrvConfig();
- final NettyServerConfig nettyServerConfig = new NettyServerConfig();
- nettyServerConfig.setListenPort(9876);
- if (commandLine.hasOption('c')) {
- String file = commandLine.getOptionValue('c');
- if (file != null) {
- InputStream in = new BufferedInputStream(new FileInputStream(file));
- properties = new Properties();
- properties.load(in);
- MixAll.properties2Object(properties, namesrvConfig);
- MixAll.properties2Object(properties, nettyServerConfig);
-
- namesrvConfig.setConfigStorePath(file);
-
- System.out.printf("load config properties file OK, %s%n", file);
- in.close();
- }
- }
-
- if (commandLine.hasOption('p')) {
- InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
- MixAll.printObjectProperties(console, namesrvConfig);
- MixAll.printObjectProperties(console, nettyServerConfig);
- System.exit(0);
- }
+ 寄生虫观后感
+ /2020/03/01/%E5%AF%84%E7%94%9F%E8%99%AB%E8%A7%82%E5%90%8E%E6%84%9F/
+ 寄生虫这部电影在获得奥斯卡之前就有关注了,豆瓣评分很高,一开始看到这个片名以为是像《铁线虫入侵》那种灾难片,后来看到男主,宋康昊,也是老面孔了,从高中时候在学校操场组织看的《汉江怪物》,有点二的感觉,后来在大学寝室电脑上重新看的时候,室友跟我说是韩国国宝级演员,真人不可貌相,感觉是个呆子的形象。
+但是你说这不是个灾难片,而是个反映社会问题的,就业比较容易往这个方向猜,只是剧情会是怎么样的,一时也没啥头绪,后来不知道哪里看了下一个剧情透露,是一个穷人给富人做家教,然后把自己一家都带进富人家,如果是这样的话可能会把这个怎么带进去作为一个主线,不过事实告诉我,这没那么重要,从第一步朋友的介绍,就显得无比顺利,要去当家教了,作为一个穷成这样的人,瞬间转变成一个衣着得体,言行举止都没让富人家看出破绽的延世大学学生,这真的挺难让人理解,所谓江山易改,本性难移,还有就是这人也正好有那么好能力去辅导,并且诡异的是,多惠也是瞬间就喜欢上了男主,多惠跟将男主介绍给她做家教,也就是多惠原来的家教敏赫,应该也有不少的相处时间,这变了有点大了吧,当然这里也可能因为时长需要,如果说这一点是因为时长,那可能我所有的槽点都是因为这个吧,因为我理解的应该是把家里的人如何一步步地带进富人家,这应该是整个剧情的一个需要更多铺垫去克服这个矛盾点,有时候也想过如果我去当导演,是能拍出个啥,没这个机会,可能有也会是很扯淡的,当然这也不能阻拦我谈谈对这个点的一些看法,毕竟评价一台电冰箱不是说我必须得自己会制冷对吧,这大概是我觉得这个电影的第一个槽点,接下去接二连三的,就是我说的这个最核心的矛盾点,不知道谁说过,这种影视剧应该是源自于生活又高于生活,越是好的作品,越要接近生活,这样子才更能有感同身受。
+接下去的点是金基宇介绍金基婷去给多颂当美术家教,这一步又是我理解的败笔吧,就怎么说呢,没什么铺垫,突然从一个社会底层的穷姑娘,转变成一个气场爆表,把富人家太太唬得一愣一愣的,如果说富太太是比较简单无脑的,那富人自己应该是比较有见识而且是做 IT 的,给自己儿子女儿做家教的,查查底细也很正常吧,但是啥都没有,然后呢,她又开始耍司机的心机了,真的是莫名其妙了,司机真的很惨,窈窕淑女君子好逑,而且这个操作也让我摸不着头脑,这是多腹黑并且有经验才会这么操作,脱内裤真的是让我看得一愣愣的,更看得我一愣一愣的,富人竟然也完全按着这个思路去想了,完全没有别的可能呢,甚至可以去查下行车记录仪或者怎样的,或者有没有毛发体液啥的去检验下,毕竟金基婷也乘坐过这辆车,但是最最让我不懂的还是脱内裤这个操作,究竟是什么样的人才会的呢,值得思考。
+金基泽和忠淑的点也是比较奇怪,首先是金基泽,引起最后那个杀人事件的一个由头,大部分观点都是人为朴社长在之前跟老婆啪啪啪的时候说金基泽的身上有股乘地铁的人的味道,简而言之就是穷人的味道,还有去雯光丈夫身下拿钥匙是对金基泽和雯光丈夫身上的味道的鄙夷,可是这个原因真的站不住脚,即使是同样经济水平,如果身上有比较重的异味,背后讨论下,或者闻到了比较重的味道,有不适的表情和动作很正常吧,像雯光丈夫,在地下室里呆了那么久,身上有异味并且比较重太正常了,就跟在厕所呆久了不会觉得味道大,但是从没味道的地方一进有点味道的厕所就会觉得异样,略尴尬的理由;再说忠淑呢,感觉是太厉害了,能胜任这么一家有钱人的各种挑剔的饮食口味要求的保姆职位,也是让人看懵逼了,看到了不禁想到一个问题,这家人开头是那么地穷,不堪,突然转变成这么地像骗子家族,如果有这么好的骗人能力,应该不会到这种地步吧,如果真的是那么穷,没能力,没志气,又怎么会突然变成这么厉害呢,一家人各司其职,把富人家唬得团团转,而这个前提是,这些人的确能胜任这四个位置,这就是我非常不能理解的点。
+然后说回这个标题,寄生虫,不知道是不是翻译过来不准确,如果真的是叫寄生虫的话,这个寄生虫智商未免也太低了,没有像新冠那样机制,致死率低一点,传染能力强一点,潜伏期也能传染,这个寄生虫第一次受到免疫系统的攻击就自爆了;还有呢,作为一个社会比较低层的打工者,乡下人,对这个审题也是不太审的清,是指这一家人是社会的寄生虫,不思进取,并且死的应该,富人是傻白甜,又有钱又善良,这是给有钱人洗地了还是啥,这个奥斯卡真不知道是怎么得的,总觉得奥斯卡,甚至低一点,豆瓣,得奖的,评分高的都是被一群“精英党”把持的,有黑人主角的,得分高;有同性恋的,得分高;结局惨的,得分高;看不懂的,得分高;就像肖申克的救赎,真不知道是哪里好了,最近看了关于明朝那些事的三杨,杨溥的经历应该比这个厉害吧,可是外国人看不懂,就像外国人不懂中国为什么有反分裂国家法,经历了鸦片战争,八国联军,抗日战争等等,其实跟外国对于黑人的权益的问题,因为有南北战争,所以极度重视这个问题,相应的中国也有自己的历史,请理解。
+简而言之我对寄生虫的评分大概 5~6 分吧。
+]]>
+
+ 生活
+ 影评
+ 2020
+
+
+ 生活
+ 影评
+ 寄生虫
+
+ public static void main(String[] args) throws InterruptedException, MQClientException {
- MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
+ /*
+ * Instantiate with specified consumer group name.
+ * 首先是new 一个对象出来,然后指定 Consumer 的 Group
+ * 同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
+ */
+ DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
- if (null == namesrvConfig.getRocketmqHome()) {
- System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
- System.exit(-2);
- }
+ /*
+ * Specify name server addresses.
+ * <p/>
+ * 这里可以通知指定环境变量或者设置对象参数的形式指定名字空间服务的地址
+ *
+ * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
+ * <pre>
+ * {@code
+ * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
+ * }
+ * </pre>
+ */
- LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
- JoranConfigurator configurator = new JoranConfigurator();
- configurator.setContext(lc);
- lc.reset();
- configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
+ /*
+ * Specify where to start in case the specified consumer group is a brand new one.
+ * 指定消费起始点
+ */
+ consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
- log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
+ /*
+ * Subscribe one more more topics to consume.
+ * 指定订阅的 topic 跟 tag,注意后面的是个表达式,可以以 tag1 || tag2 || tag3 传入
+ */
+ consumer.subscribe("TopicTest", "*");
- MixAll.printObjectProperties(log, namesrvConfig);
- MixAll.printObjectProperties(log, nettyServerConfig);
+ /*
+ * Register callback to execute on arrival of messages fetched from brokers.
+ * 注册具体获得消息后的处理方法
+ */
+ consumer.registerMessageListener(new MessageListenerConcurrently() {
- final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
+ @Override
+ public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
+ ConsumeConcurrentlyContext context) {
+ System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
+ return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
+ }
+ });
- // remember all configs to prevent discard
- controller.getConfiguration().registerConfig(properties);
+ /*
+ * Launch the consumer instance.
+ * 启动消费者
+ */
+ consumer.start();
- return controller;
- }
+ System.out.printf("Consumer Started.%n");
+ }
-这个方法里其实主要是读取一些配置啥的,不是很复杂,
-public static NamesrvController start(final NamesrvController controller) throws Exception {
+然后就是看看 start 的过程了
+/**
+ * This method gets internal infrastructure readily to serve. Instances must call this method after configuration.
+ *
+ * @throws MQClientException if there is any client error.
+ */
+ @Override
+ public void start() throws MQClientException {
+ setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
+ this.defaultMQPushConsumerImpl.start();
+ if (null != traceDispatcher) {
+ try {
+ traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
+ } catch (MQClientException e) {
+ log.warn("trace dispatcher start failed ", e);
+ }
+ }
+ }
+具体的逻辑在this.defaultMQPushConsumerImpl.start(),这个 defaultMQPushConsumerImpl 就是
+/**
+ * Internal implementation. Most of the functions herein are delegated to it.
+ */
+ protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
- if (null == controller) {
- throw new IllegalArgumentException("NamesrvController is null");
- }
+public synchronized void start() throws MQClientException {
+ switch (this.serviceState) {
+ case CREATE_JUST:
+ log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
+ this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
+ // 这里比较巧妙,相当于想设立了个屏障,防止并发启动,不过这里并不是悲观锁,也不算个严格的乐观锁
+ this.serviceState = ServiceState.START_FAILED;
- boolean initResult = controller.initialize();
- if (!initResult) {
- controller.shutdown();
- System.exit(-3);
- }
+ this.checkConfig();
- Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- controller.shutdown();
- return null;
- }
- }));
+ this.copySubscription();
- controller.start();
+ if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
+ this.defaultMQPushConsumer.changeInstanceNameToPID();
+ }
- return controller;
- }
+ // 这个mQClientFactory,负责管理client(consumer、producer),并提供多中功能接口供各个Service(Rebalance、PullMessage等)调用;大部分逻辑均在这个类中完成
+ this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
-这个start里主要关注initialize方法,后面就是一个停机的hook,来看下initialize方法
-public boolean initialize() {
+ // 这个 rebalanceImpl 主要负责决定,当前的consumer应该从哪些Queue中消费消息;
+ this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
+ this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
+ this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
+ this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
- this.kvConfigManager.load();
+ // 长连接,负责从broker处拉取消息,然后利用ConsumeMessageService回调用户的Listener执行消息消费逻辑
+ this.pullAPIWrapper = new PullAPIWrapper(
+ mQClientFactory,
+ this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
+ this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
- this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
+ if (this.defaultMQPushConsumer.getOffsetStore() != null) {
+ this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
+ } else {
+ switch (this.defaultMQPushConsumer.getMessageModel()) {
+ case BROADCASTING:
+ this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
+ break;
+ case CLUSTERING:
+ this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
+ break;
+ default:
+ break;
+ }
+ this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
+ }
+ // offsetStore 维护当前consumer的消费记录(offset);有两种实现,Local和Rmote,Local存储在本地磁盘上,适用于BROADCASTING广播消费模式;而Remote则将消费进度存储在Broker上,适用于CLUSTERING集群消费模式;
+ this.offsetStore.load();
- this.remotingExecutor =
- Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
+ if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
+ this.consumeOrderly = true;
+ this.consumeMessageService =
+ new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
+ } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
+ this.consumeOrderly = false;
+ this.consumeMessageService =
+ new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
+ }
- this.registerProcessor();
+ // 实现所谓的"Push-被动"消费机制;从Broker拉取的消息后,封装成ConsumeRequest提交给ConsumeMessageSerivce,此service负责回调用户的Listener消费消息;
+ this.consumeMessageService.start();
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
+ if (!registerOK) {
+ this.serviceState = ServiceState.CREATE_JUST;
+ this.consumeMessageService.shutdown();
+ throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+ + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
+ null);
+ }
- @Override
- public void run() {
- NamesrvController.this.routeInfoManager.scanNotActiveBroker();
- }
- }, 5, 10, TimeUnit.SECONDS);
+ mQClientFactory.start();
+ log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
+ this.serviceState = ServiceState.RUNNING;
+ break;
+ case RUNNING:
+ case START_FAILED:
+ case SHUTDOWN_ALREADY:
+ throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
+ + this.serviceState
+ + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
+ null);
+ default:
+ break;
+ }
- this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ this.updateTopicSubscribeInfoWhenSubscriptionChanged();
+ this.mQClientFactory.checkClientInBroker();
+ this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
+ this.mQClientFactory.rebalanceImmediately();
+ }
+然后我们往下看主要的目光聚焦mQClientFactory.start()
+public void start() throws MQClientException {
- @Override
- public void run() {
- NamesrvController.this.kvConfigManager.printAllPeriodically();
- }
- }, 1, 10, TimeUnit.MINUTES);
+ synchronized (this) {
+ switch (this.serviceState) {
+ case CREATE_JUST:
+ this.serviceState = ServiceState.START_FAILED;
+ // If not specified,looking address from name server
+ if (null == this.clientConfig.getNamesrvAddr()) {
+ this.mQClientAPIImpl.fetchNameServerAddr();
+ }
+ // Start request-response channel
+ // 这里主要是初始化了个网络客户端
+ this.mQClientAPIImpl.start();
+ // Start various schedule tasks
+ // 定时任务
+ this.startScheduledTask();
+ // Start pull service
+ // 这里重点说下
+ this.pullMessageService.start();
+ // Start rebalance service
+ this.rebalanceService.start();
+ // Start push service
+ this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
+ log.info("the client factory [{}] start OK", this.clientId);
+ this.serviceState = ServiceState.RUNNING;
+ break;
+ case START_FAILED:
+ throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
+ default:
+ break;
+ }
+ }
+ }
+我们来看下这个 pullMessageService,org.apache.rocketmq.client.impl.consumer.PullMessageService,
![]()
实现了 runnable 接口,
然后可以看到 run 方法
+public void run() {
+ log.info(this.getServiceName() + " service started");
- if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
- // Register a listener to reload SslContext
- try {
- fileWatchService = new FileWatchService(
- new String[] {
- TlsSystemConfig.tlsServerCertPath,
- TlsSystemConfig.tlsServerKeyPath,
- TlsSystemConfig.tlsServerTrustCertPath
- },
- new FileWatchService.Listener() {
- boolean certChanged, keyChanged = false;
- @Override
- public void onChanged(String path) {
- if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
- log.info("The trust certificate changed, reload the ssl context");
- reloadServerSslContext();
- }
- if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
- certChanged = true;
- }
- if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
- keyChanged = true;
- }
- if (certChanged && keyChanged) {
- log.info("The certificate and private key changed, reload the ssl context");
- certChanged = keyChanged = false;
- reloadServerSslContext();
- }
- }
- private void reloadServerSslContext() {
- ((NettyRemotingServer) remotingServer).loadSslContext();
- }
- });
- } catch (Exception e) {
- log.warn("FileWatchService created error, can't load the certificate dynamically");
- }
- }
+ while (!this.isStopped()) {
+ try {
+ PullRequest pullRequest = this.pullRequestQueue.take();
+ this.pullMessage(pullRequest);
+ } catch (InterruptedException ignored) {
+ } catch (Exception e) {
+ log.error("Pull Message Service Run Method exception", e);
+ }
+ }
- return true;
- }
+ log.info(this.getServiceName() + " service end");
+ }
+接着在看 pullMessage 方法
+private void pullMessage(final PullRequest pullRequest) {
+ final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
+ if (consumer != null) {
+ DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
+ impl.pullMessage(pullRequest);
+ } else {
+ log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
+ }
+ }
+实际上调用了这个方法,这个方法很长,我在代码里注释下下每一段的功能
+public void pullMessage(final PullRequest pullRequest) {
+ final ProcessQueue processQueue = pullRequest.getProcessQueue();
+ // 这里开始就是检查状态,确定是否往下执行
+ if (processQueue.isDropped()) {
+ log.info("the pull request[{}] is dropped.", pullRequest.toString());
+ return;
+ }
-这里的kvConfigManager主要是来加载NameServer的配置参数,存到org.apache.rocketmq.namesrv.kvconfig.KVConfigManager#configTable中,然后是以BrokerHousekeepingService对象为参数初始化NettyRemotingServer对象,BrokerHousekeepingService对象作为该Netty连接中Socket链接的监听器(ChannelEventListener);监听与Broker建立的渠道的状态(空闲、关闭、异常三个状态),并调用BrokerHousekeepingService的相应onChannel方法。其中渠道的空闲、关闭、异常状态均调用RouteInfoManager.onChannelDestory方法处理。这个BrokerHousekeepingService可以字面化地理解为broker的管家服务,这个类内部三个状态方法其实都是调用的org.apache.rocketmq.namesrv.NamesrvController#getRouteInfoManager方法,而这个RouteInfoManager里面的对象有这些
-public class RouteInfoManager {
- private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
- private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
- private final ReadWriteLock lock = new ReentrantReadWriteLock();
- // topic与queue的对应关系
- private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
- // Broker名称与broker属性的map
- private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
- // 集群与broker集合的对应关系
- private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
- // 活跃的broker信息
- private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
- // Broker地址与过滤器
- private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
+ pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
-然后接下去就是初始化了一个线程池,然后注册默认的处理类this.registerProcessor();默认都是这个处理器去处理请求 org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#DefaultRequestProcessor然后是初始化两个定时任务
-第一是每10秒检查一遍Broker的状态的定时任务,调用scanNotActiveBroker方法;遍历brokerLiveTable集合,查看每个broker的最后更新时间(BrokerLiveInfo.lastUpdateTimestamp)是否超过2分钟,若超过则关闭该broker的渠道并调用RouteInfoManager.onChannelDestory方法清理RouteInfoManager类的topicQueueTable、brokerAddrTable、clusterAddrTable、filterServerTable成员变量。
-this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ try {
+ this.makeSureStateOK();
+ } catch (MQClientException e) {
+ log.warn("pullMessage exception, consumer state not ok", e);
+ this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+ return;
+ }
- @Override
- public void run() {
- NamesrvController.this.routeInfoManager.scanNotActiveBroker();
- }
- }, 5, 10, TimeUnit.SECONDS);
-public void scanNotActiveBroker() {
- Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
- while (it.hasNext()) {
- Entry<String, BrokerLiveInfo> next = it.next();
- long last = next.getValue().getLastUpdateTimestamp();
- if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
- RemotingUtil.closeChannel(next.getValue().getChannel());
- it.remove();
- log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
- this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
- }
- }
- }
+ if (this.isPause()) {
+ log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
+ this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
+ return;
+ }
- public void onChannelDestroy(String remoteAddr, Channel channel) {
- String brokerAddrFound = null;
- if (channel != null) {
- try {
- try {
- this.lock.readLock().lockInterruptibly();
- Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
- this.brokerLiveTable.entrySet().iterator();
- while (itBrokerLiveTable.hasNext()) {
- Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
- if (entry.getValue().getChannel() == channel) {
- brokerAddrFound = entry.getKey();
- break;
- }
- }
- } finally {
- this.lock.readLock().unlock();
- }
- } catch (Exception e) {
- log.error("onChannelDestroy Exception", e);
- }
- }
+ // 这块其实是个类似于限流的功能块,对消息数量和消息大小做限制
+ long cachedMessageCount = processQueue.getMsgCount().get();
+ long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
- if (null == brokerAddrFound) {
- brokerAddrFound = remoteAddr;
- } else {
- log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
- }
+ if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
+ this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
+ if ((queueFlowControlTimes++ % 1000) == 0) {
+ log.warn(
+ "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
+ this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
+ }
+ return;
+ }
- if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
+ if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
+ this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
+ if ((queueFlowControlTimes++ % 1000) == 0) {
+ log.warn(
+ "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
+ this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
+ }
+ return;
+ }
- try {
- try {
- this.lock.writeLock().lockInterruptibly();
- this.brokerLiveTable.remove(brokerAddrFound);
- this.filterServerTable.remove(brokerAddrFound);
- String brokerNameFound = null;
- boolean removeBrokerName = false;
- Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
- this.brokerAddrTable.entrySet().iterator();
- while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
- BrokerData brokerData = itBrokerAddrTable.next().getValue();
+ // 若不是顺序消费(即DefaultMQPushConsumerImpl.consumeOrderly等于false),则检查ProcessQueue对象的msgTreeMap:TreeMap<Long,MessageExt>变量的第一个key值与最后一个key值之间的差额,该key值表示查询的队列偏移量queueoffset;若差额大于阈值(由DefaultMQPushConsumer. consumeConcurrentlyMaxSpan指定,默认是2000),则调用PullMessageService.executePullRequestLater方法,在50毫秒之后重新将该PullRequest请求放入PullMessageService.pullRequestQueue队列中;并跳出该方法;这里的意思主要就是消息有堆积了,等会再来拉取
+ if (!this.consumeOrderly) {
+ if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
+ this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
+ if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
+ log.warn(
+ "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
+ processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
+ pullRequest, queueMaxSpanFlowControlTimes);
+ }
+ return;
+ }
+ } else {
+ if (processQueue.isLocked()) {
+ if (!pullRequest.isLockedFirst()) {
+ final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
+ boolean brokerBusy = offset < pullRequest.getNextOffset();
+ log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
+ pullRequest, offset, brokerBusy);
+ if (brokerBusy) {
+ log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
+ pullRequest, offset);
+ }
- Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
- while (it.hasNext()) {
- Entry<Long, String> entry = it.next();
- Long brokerId = entry.getKey();
- String brokerAddr = entry.getValue();
- if (brokerAddr.equals(brokerAddrFound)) {
- brokerNameFound = brokerData.getBrokerName();
- it.remove();
- log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
- brokerId, brokerAddr);
- break;
- }
- }
+ pullRequest.setLockedFirst(true);
+ pullRequest.setNextOffset(offset);
+ }
+ } else {
+ this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+ log.info("pull message later because not locked in broker, {}", pullRequest);
+ return;
+ }
+ }
- if (brokerData.getBrokerAddrs().isEmpty()) {
- removeBrokerName = true;
- itBrokerAddrTable.remove();
- log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
- brokerData.getBrokerName());
- }
- }
+ // 以PullRequest.messageQueue对象的topic值为参数从RebalanceImpl.subscriptionInner: ConcurrentHashMap, SubscriptionData>中获取对应的SubscriptionData对象,若该对象为null,考虑到并发的关系,调用executePullRequestLater方法,稍后重试;并跳出该方法;
+ final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
+ if (null == subscriptionData) {
+ this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+ log.warn("find the consumer's subscription failed, {}", pullRequest);
+ return;
+ }
- if (brokerNameFound != null && removeBrokerName) {
- Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
- while (it.hasNext()) {
- Entry<String, Set<String>> entry = it.next();
- String clusterName = entry.getKey();
- Set<String> brokerNames = entry.getValue();
- boolean removed = brokerNames.remove(brokerNameFound);
- if (removed) {
- log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
- brokerNameFound, clusterName);
+ final long beginTimestamp = System.currentTimeMillis();
- if (brokerNames.isEmpty()) {
- log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
- clusterName);
- it.remove();
- }
+ // 异步拉取回调,先不讨论细节
+ PullCallback pullCallback = new PullCallback() {
+ @Override
+ public void onSuccess(PullResult pullResult) {
+ if (pullResult != null) {
+ pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
+ subscriptionData);
- break;
- }
- }
- }
+ switch (pullResult.getPullStatus()) {
+ case FOUND:
+ long prevRequestOffset = pullRequest.getNextOffset();
+ pullRequest.setNextOffset(pullResult.getNextBeginOffset());
+ long pullRT = System.currentTimeMillis() - beginTimestamp;
+ DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
+ pullRequest.getMessageQueue().getTopic(), pullRT);
- if (removeBrokerName) {
- Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
- this.topicQueueTable.entrySet().iterator();
- while (itTopicQueueTable.hasNext()) {
- Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
- String topic = entry.getKey();
- List<QueueData> queueDataList = entry.getValue();
+ long firstMsgOffset = Long.MAX_VALUE;
+ if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
+ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+ } else {
+ firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
- Iterator<QueueData> itQueueData = queueDataList.iterator();
- while (itQueueData.hasNext()) {
- QueueData queueData = itQueueData.next();
- if (queueData.getBrokerName().equals(brokerNameFound)) {
- itQueueData.remove();
- log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
- topic, queueData);
- }
- }
+ DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
+ pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
- if (queueDataList.isEmpty()) {
- itTopicQueueTable.remove();
- log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
- topic);
- }
- }
- }
- } finally {
- this.lock.writeLock().unlock();
- }
- } catch (Exception e) {
- log.error("onChannelDestroy Exception", e);
- }
- }
- }
+ boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
+ DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
+ pullResult.getMsgFoundList(),
+ processQueue,
+ pullRequest.getMessageQueue(),
+ dispatchToConsume);
-第二个是每10分钟打印一次NameServer的配置参数。即KVConfigManager.configTable变量的内容。
-this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
+ DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
+ DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
+ } else {
+ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+ }
+ }
- @Override
- public void run() {
- NamesrvController.this.kvConfigManager.printAllPeriodically();
- }
- }, 1, 10, TimeUnit.MINUTES);
+ if (pullResult.getNextBeginOffset() < prevRequestOffset
+ || firstMsgOffset < prevRequestOffset) {
+ log.warn(
+ "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
+ pullResult.getNextBeginOffset(),
+ firstMsgOffset,
+ prevRequestOffset);
+ }
-然后这个初始化就差不多完成了,后面只需要把remotingServer start一下就好了
-处理请求
直接上代码,其实主体是swtich case去判断
-@Override
- public RemotingCommand processRequest(ChannelHandlerContext ctx,
- RemotingCommand request) throws RemotingCommandException {
+ break;
+ case NO_NEW_MSG:
+ pullRequest.setNextOffset(pullResult.getNextBeginOffset());
- if (ctx != null) {
- log.debug("receive request, {} {} {}",
- request.getCode(),
- RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
- request);
- }
+ DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
+ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+ break;
+ case NO_MATCHED_MSG:
+ pullRequest.setNextOffset(pullResult.getNextBeginOffset());
- switch (request.getCode()) {
- case RequestCode.PUT_KV_CONFIG:
- return this.putKVConfig(ctx, request);
- case RequestCode.GET_KV_CONFIG:
- return this.getKVConfig(ctx, request);
- case RequestCode.DELETE_KV_CONFIG:
- return this.deleteKVConfig(ctx, request);
- case RequestCode.QUERY_DATA_VERSION:
- return queryBrokerTopicConfig(ctx, request);
- case RequestCode.REGISTER_BROKER:
- Version brokerVersion = MQVersion.value2Version(request.getVersion());
- if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
- return this.registerBrokerWithFilterServer(ctx, request);
- } else {
- return this.registerBroker(ctx, request);
- }
- case RequestCode.UNREGISTER_BROKER:
- return this.unregisterBroker(ctx, request);
- case RequestCode.GET_ROUTEINTO_BY_TOPIC:
- return this.getRouteInfoByTopic(ctx, request);
- case RequestCode.GET_BROKER_CLUSTER_INFO:
- return this.getBrokerClusterInfo(ctx, request);
- case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
- return this.wipeWritePermOfBroker(ctx, request);
- case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
- return getAllTopicListFromNameserver(ctx, request);
- case RequestCode.DELETE_TOPIC_IN_NAMESRV:
- return deleteTopicInNamesrv(ctx, request);
- case RequestCode.GET_KVLIST_BY_NAMESPACE:
- return this.getKVListByNamespace(ctx, request);
- case RequestCode.GET_TOPICS_BY_CLUSTER:
- return this.getTopicsByCluster(ctx, request);
- case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
- return this.getSystemTopicListFromNs(ctx, request);
- case RequestCode.GET_UNIT_TOPIC_LIST:
- return this.getUnitTopicList(ctx, request);
- case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
- return this.getHasUnitSubTopicList(ctx, request);
- case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
- return this.getHasUnitSubUnUnitTopicList(ctx, request);
- case RequestCode.UPDATE_NAMESRV_CONFIG:
- return this.updateConfig(ctx, request);
- case RequestCode.GET_NAMESRV_CONFIG:
- return this.getConfig(ctx, request);
- default:
- break;
- }
- return null;
- }
+ DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
-以broker注册为例,
-case RequestCode.REGISTER_BROKER:
- Version brokerVersion = MQVersion.value2Version(request.getVersion());
- if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
- return this.registerBrokerWithFilterServer(ctx, request);
- } else {
- return this.registerBroker(ctx, request);
- }
+ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
+ break;
+ case OFFSET_ILLEGAL:
+ log.warn("the pull request offset illegal, {} {}",
+ pullRequest.toString(), pullResult.toString());
+ pullRequest.setNextOffset(pullResult.getNextBeginOffset());
-做了个简单的版本管理,我们看下前面一个的代码
-public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
- throws RemotingCommandException {
- final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
- final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
- final RegisterBrokerRequestHeader requestHeader =
- (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);
+ pullRequest.getProcessQueue().setDropped(true);
+ DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
- if (!checksum(ctx, request, requestHeader)) {
- response.setCode(ResponseCode.SYSTEM_ERROR);
- response.setRemark("crc32 not match");
- return response;
- }
+ @Override
+ public void run() {
+ try {
+ DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
+ pullRequest.getNextOffset(), false);
- RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
+ DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
- if (request.getBody() != null) {
- try {
- registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
- } catch (Exception e) {
- throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
- }
- } else {
- registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
- registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
- }
+ DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
- RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
- requestHeader.getClusterName(),
- requestHeader.getBrokerAddr(),
- requestHeader.getBrokerName(),
- requestHeader.getBrokerId(),
- requestHeader.getHaServerAddr(),
- registerBrokerBody.getTopicConfigSerializeWrapper(),
- registerBrokerBody.getFilterServerList(),
- ctx.channel());
+ log.warn("fix the pull request offset, {}", pullRequest);
+ } catch (Throwable e) {
+ log.error("executeTaskLater Exception", e);
+ }
+ }
+ }, 10000);
+ break;
+ default:
+ break;
+ }
+ }
+ }
- responseHeader.setHaServerAddr(result.getHaServerAddr());
- responseHeader.setMasterAddr(result.getMasterAddr());
+ @Override
+ public void onException(Throwable e) {
+ if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
+ log.warn("execute the pull request exception", e);
+ }
- byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
- response.setBody(jsonValue);
+ DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+ }
+ };
+ // 如果为集群模式,即可置commitOffsetEnable为 true
+ boolean commitOffsetEnable = false;
+ long commitOffsetValue = 0L;
+ if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
+ commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
+ if (commitOffsetValue > 0) {
+ commitOffsetEnable = true;
+ }
+ }
- response.setCode(ResponseCode.SUCCESS);
- response.setRemark(null);
- return response;
-}
+ // 将上面获得的commitOffsetEnable更新到订阅关系里
+ String subExpression = null;
+ boolean classFilter = false;
+ SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
+ if (sd != null) {
+ if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) {
+ subExpression = sd.getSubString();
+ }
-可以看到主要的逻辑还是在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker这个方法里
-public RegisterBrokerResult registerBroker(
- final String clusterName,
- final String brokerAddr,
- final String brokerName,
- final long brokerId,
- final String haServerAddr,
- final TopicConfigSerializeWrapper topicConfigWrapper,
- final List<String> filterServerList,
- final Channel channel) {
- RegisterBrokerResult result = new RegisterBrokerResult();
- try {
- try {
- this.lock.writeLock().lockInterruptibly();
+ classFilter = sd.isClassFilterMode();
+ }
- // 更新这个clusterAddrTable
- Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
- if (null == brokerNames) {
- brokerNames = new HashSet<String>();
- this.clusterAddrTable.put(clusterName, brokerNames);
- }
- brokerNames.add(brokerName);
+ // 组成 sysFlag
+ int sysFlag = PullSysFlag.buildSysFlag(
+ commitOffsetEnable, // commitOffset
+ true, // suspend
+ subExpression != null, // subscription
+ classFilter // class filter
+ );
+ // 调用真正的拉取消息接口
+ try {
+ this.pullAPIWrapper.pullKernelImpl(
+ pullRequest.getMessageQueue(),
+ subExpression,
+ subscriptionData.getExpressionType(),
+ subscriptionData.getSubVersion(),
+ pullRequest.getNextOffset(),
+ this.defaultMQPushConsumer.getPullBatchSize(),
+ sysFlag,
+ commitOffsetValue,
+ BROKER_SUSPEND_MAX_TIME_MILLIS,
+ CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
+ CommunicationMode.ASYNC,
+ pullCallback
+ );
+ } catch (Exception e) {
+ log.error("pullKernelImpl exception", e);
+ this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
+ }
+ }
+以下就是拉取消息的底层 api,不够不是特别复杂,主要是在找 broker,和设置请求参数
+public PullResult pullKernelImpl(
+ final MessageQueue mq,
+ final String subExpression,
+ final String expressionType,
+ final long subVersion,
+ final long offset,
+ final int maxNums,
+ final int sysFlag,
+ final long commitOffset,
+ final long brokerSuspendMaxTimeMillis,
+ final long timeoutMillis,
+ final CommunicationMode communicationMode,
+ final PullCallback pullCallback
+) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
+ FindBrokerResult findBrokerResult =
+ this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
+ this.recalculatePullFromWhichNode(mq), false);
+ if (null == findBrokerResult) {
+ this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
+ findBrokerResult =
+ this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
+ this.recalculatePullFromWhichNode(mq), false);
+ }
- boolean registerFirst = false;
+ if (findBrokerResult != null) {
+ {
+ // check version
+ if (!ExpressionType.isTagType(expressionType)
+ && findBrokerResult.getBrokerVersion() < MQVersion.Version.V4_1_0_SNAPSHOT.ordinal()) {
+ throw new MQClientException("The broker[" + mq.getBrokerName() + ", "
+ + findBrokerResult.getBrokerVersion() + "] does not upgrade to support for filter message by " + expressionType, null);
+ }
+ }
+ int sysFlagInner = sysFlag;
+
+ if (findBrokerResult.isSlave()) {
+ sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner);
+ }
+
+ PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
+ requestHeader.setConsumerGroup(this.consumerGroup);
+ requestHeader.setTopic(mq.getTopic());
+ requestHeader.setQueueId(mq.getQueueId());
+ requestHeader.setQueueOffset(offset);
+ requestHeader.setMaxMsgNums(maxNums);
+ requestHeader.setSysFlag(sysFlagInner);
+ requestHeader.setCommitOffset(commitOffset);
+ requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);
+ requestHeader.setSubscription(subExpression);
+ requestHeader.setSubVersion(subVersion);
+ requestHeader.setExpressionType(expressionType);
- // 更新brokerAddrTable
- BrokerData brokerData = this.brokerAddrTable.get(brokerName);
- if (null == brokerData) {
- registerFirst = true;
- brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
- this.brokerAddrTable.put(brokerName, brokerData);
- }
- Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
- //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
- //The same IP:PORT must only have one record in brokerAddrTable
- Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
- while (it.hasNext()) {
- Entry<Long, String> item = it.next();
- if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
- it.remove();
- }
- }
+ String brokerAddr = findBrokerResult.getBrokerAddr();
+ if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
+ brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
+ }
- String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
- registerFirst = registerFirst || (null == oldAddr);
+ PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
+ brokerAddr,
+ requestHeader,
+ timeoutMillis,
+ communicationMode,
+ pullCallback);
- // 更新了org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable中的数据
- if (null != topicConfigWrapper
- && MixAll.MASTER_ID == brokerId) {
- if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
- || registerFirst) {
- ConcurrentMap<String, TopicConfig> tcTable =
- topicConfigWrapper.getTopicConfigTable();
- if (tcTable != null) {
- for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
- this.createAndUpdateQueueData(brokerName, entry.getValue());
- }
- }
- }
- }
+ return pullResult;
+ }
- // 更新活跃broker信息
- BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
- new BrokerLiveInfo(
- System.currentTimeMillis(),
- topicConfigWrapper.getDataVersion(),
- channel,
- haServerAddr));
- if (null == prevBrokerLiveInfo) {
- log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
- }
+ throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
+}
+再看下一步的
+public PullResult pullMessage(
+ final String addr,
+ final PullMessageRequestHeader requestHeader,
+ final long timeoutMillis,
+ final CommunicationMode communicationMode,
+ final PullCallback pullCallback
+) throws RemotingException, MQBrokerException, InterruptedException {
+ RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);
- // 处理filter
- if (filterServerList != null) {
- if (filterServerList.isEmpty()) {
- this.filterServerTable.remove(brokerAddr);
- } else {
- this.filterServerTable.put(brokerAddr, filterServerList);
- }
- }
+ switch (communicationMode) {
+ case ONEWAY:
+ assert false;
+ return null;
+ case ASYNC:
+ this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
+ return null;
+ case SYNC:
+ return this.pullMessageSync(addr, request, timeoutMillis);
+ default:
+ assert false;
+ break;
+ }
- // 当当前broker非master时返回master信息
- if (MixAll.MASTER_ID != brokerId) {
- String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
- if (masterAddr != null) {
- BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
- if (brokerLiveInfo != null) {
- result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
- result.setMasterAddr(masterAddr);
- }
- }
- }
- } finally {
- this.lock.writeLock().unlock();
- }
- } catch (Exception e) {
- log.error("registerBroker Exception", e);
- }
+ return null;
+}
+通过 communicationMode 判断是同步拉取还是异步拉取,异步就调用
+private void pullMessageAsync(
+ final String addr,
+ final RemotingCommand request,
+ final long timeoutMillis,
+ final PullCallback pullCallback
+ ) throws RemotingException, InterruptedException {
+ this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
+ @Override
+ public void operationComplete(ResponseFuture responseFuture) {
+ 异步
+ RemotingCommand response = responseFuture.getResponseCommand();
+ if (response != null) {
+ try {
+ PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response);
+ assert pullResult != null;
+ pullCallback.onSuccess(pullResult);
+ } catch (Exception e) {
+ pullCallback.onException(e);
+ }
+ } else {
+ if (!responseFuture.isSendRequestOK()) {
+ pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause()));
+ } else if (responseFuture.isTimeout()) {
+ pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request,
+ responseFuture.getCause()));
+ } else {
+ pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause()));
+ }
+ }
+ }
+ });
+ }
+并且会调用前面 pullCallback 的onSuccess和onException方法,同步的就是调用
+private PullResult pullMessageSync(
+ final String addr,
+ final RemotingCommand request,
+ final long timeoutMillis
+ ) throws RemotingException, InterruptedException, MQBrokerException {
+ RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
+ assert response != null;
+ return this.processPullResponse(response);
+ }
+然后就是这个 remotingClient 的 invokeAsync 跟 invokeSync 方法
+@Override
+ public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
+ throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
+ RemotingSendRequestException {
+ long beginStartTime = System.currentTimeMillis();
+ final Channel channel = this.getAndCreateChannel(addr);
+ if (channel != null && channel.isActive()) {
+ try {
+ doBeforeRpcHooks(addr, request);
+ long costTime = System.currentTimeMillis() - beginStartTime;
+ if (timeoutMillis < costTime) {
+ throw new RemotingTooMuchRequestException("invokeAsync call timeout");
+ }
+ this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, invokeCallback);
+ } catch (RemotingSendRequestException e) {
+ log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
+ this.closeChannel(addr, channel);
+ throw e;
+ }
+ } else {
+ this.closeChannel(addr, channel);
+ throw new RemotingConnectException(addr);
+ }
+ }
+@Override
+ public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
+ throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
+ long beginStartTime = System.currentTimeMillis();
+ final Channel channel = this.getAndCreateChannel(addr);
+ if (channel != null && channel.isActive()) {
+ try {
+ doBeforeRpcHooks(addr, request);
+ long costTime = System.currentTimeMillis() - beginStartTime;
+ if (timeoutMillis < costTime) {
+ throw new RemotingTimeoutException("invokeSync call timeout");
+ }
+ RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
+ doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
+ return response;
+ } catch (RemotingSendRequestException e) {
+ log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
+ this.closeChannel(addr, channel);
+ throw e;
+ } catch (RemotingTimeoutException e) {
+ if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
+ this.closeChannel(addr, channel);
+ log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
+ }
+ log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
+ throw e;
+ }
+ } else {
+ this.closeChannel(addr, channel);
+ throw new RemotingConnectException(addr);
+ }
+ }
+再往下看
+public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
+ final long timeoutMillis)
+ throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
+ final int opaque = request.getOpaque();
- return result;
- }
+ try {
+ 同步跟异步都是会把结果用ResponseFuture抱起来
+ final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
+ this.responseTable.put(opaque, responseFuture);
+ final SocketAddress addr = channel.remoteAddress();
+ channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
+ @Override
+ public void operationComplete(ChannelFuture f) throws Exception {
+ if (f.isSuccess()) {
+ responseFuture.setSendRequestOK(true);
+ return;
+ } else {
+ responseFuture.setSendRequestOK(false);
+ }
-这个是注册 broker 的逻辑,再看下根据 topic 获取 broker 信息和 topic 信息,org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic 主要是这个方法的逻辑
-public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
- RemotingCommand request) throws RemotingCommandException {
- final RemotingCommand response = RemotingCommand.createResponseCommand(null);
- final GetRouteInfoRequestHeader requestHeader =
- (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
+ responseTable.remove(opaque);
+ responseFuture.setCause(f.cause());
+ responseFuture.putResponse(null);
+ log.warn("send a request command to channel <" + addr + "> failed.");
+ }
+ });
+ // 区别是同步的是在这等待
+ RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
+ if (null == responseCommand) {
+ if (responseFuture.isSendRequestOK()) {
+ throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
+ responseFuture.getCause());
+ } else {
+ throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
+ }
+ }
- TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
+ return responseCommand;
+ } finally {
+ this.responseTable.remove(opaque);
+ }
+ }
- if (topicRouteData != null) {
- if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
- String orderTopicConf =
- this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
- requestHeader.getTopic());
- topicRouteData.setOrderTopicConf(orderTopicConf);
- }
+ public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
+ final InvokeCallback invokeCallback)
+ throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
+ long beginStartTime = System.currentTimeMillis();
+ final int opaque = request.getOpaque();
+ boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
+ if (acquired) {
+ final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
+ long costTime = System.currentTimeMillis() - beginStartTime;
+ if (timeoutMillis < costTime) {
+ once.release();
+ throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
+ }
+
+ final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
+ this.responseTable.put(opaque, responseFuture);
+ try {
+ channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
+ @Override
+ public void operationComplete(ChannelFuture f) throws Exception {
+ if (f.isSuccess()) {
+ responseFuture.setSendRequestOK(true);
+ return;
+ }
+ requestFail(opaque);
+ log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
+ }
+ });
+ } catch (Exception e) {
+ responseFuture.release();
+ log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
+ throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
+ }
+ } else {
+ if (timeoutMillis <= 0) {
+ throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
+ } else {
+ String info =
+ String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
+ timeoutMillis,
+ this.semaphoreAsync.getQueueLength(),
+ this.semaphoreAsync.availablePermits()
+ );
+ log.warn(info);
+ throw new RemotingTimeoutException(info);
+ }
+ }
+ }
- byte[] content = topicRouteData.encode();
- response.setBody(content);
- response.setCode(ResponseCode.SUCCESS);
- response.setRemark(null);
- return response;
- }
- response.setCode(ResponseCode.TOPIC_NOT_EXIST);
- response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
- + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
- return response;
- }
-首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteData从org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。
简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。
]]>read(file, tmp_buf, len);
-write(socket, tmp_buf, len);
-
-
-
-
如上面的图显示的,要在用户态跟内核态进行切换,数据还需要在内核缓冲跟用户缓冲之间拷贝多次,
----
-- 第一步是调用 read,需要在用户态切换成内核态,DMA模块从磁盘中读取文件,并存储在内核缓冲区,相当于是第一次复制
-- 数据从内核缓冲区被拷贝到用户缓冲区,read 调用返回,伴随着内核态又切换成用户态,完成了第二次复制
-- 然后是write 写入,这里也会伴随着用户态跟内核态的切换,数据从用户缓冲区被复制到内核空间缓冲区,完成了第三次复制,这次有点不一样的是数据不是在内核缓冲区了,会复制到 socket buffer 中。
-- write 系统调用返回,又切换回了用户态,然后数据由 DMA 拷贝到协议引擎。
-
如此就能看出其实默认的读写操作代价是非常大的,而在 rocketmq 等高性能中间件中都有使用的零拷贝技术,其中 rocketmq 使用的是 mmap
-mmap基于 OS 的 mmap 的内存映射技术,通过MMU 映射文件,将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作,使随机读写文件和读写内存相似的速度。
---mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。
-
tmp_buf = mmap(file, len);
-write(socket, tmp_buf, len);
-
-
--]]>第一步:mmap系统调用使得文件内容被DMA引擎复制到内核缓冲区。然后该缓冲区与用户进程共享,在内核和用户内存空间之间不进行任何拷贝。
-第二步:写系统调用使得内核将数据从原来的内核缓冲区复制到与套接字相关的内核缓冲区。
-第三步:第三次拷贝发生在DMA引擎将数据从内核套接字缓冲区传递给协议引擎时。
-通过使用mmap而不是read,我们将内核需要拷贝的数据量减少了一半。当大量的数据被传输时,这将有很好的效果。然而,这种改进并不是没有代价的;在使用mmap+write方法时,有一些隐藏的陷阱。例如当你对一个文件进行内存映射,然后在另一个进程截断同一文件时调用写。你的写系统调用将被总线错误信号SIGBUS打断,因为你执行了一个错误的内存访问。该信号的默认行为是杀死进程并dumpcore–这对网络服务器来说不是最理想的操作。
-有两种方法可以解决这个问题。
-第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用返回。通过这样做,写系统调用会返回它在被打断之前所写的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,一个治标不治本的解决方案。因为SIGBUS预示着进程出了严重的问题,所以不鼓励使用这种解决方案。
-第二个解决方案涉及内核的文件租赁(在Windows中称为 “机会锁”)。这是解决这个问题的正确方法。通过在文件描述符上使用租赁,你与内核在一个特定的文件上达成租约。然后你可以向内核请求一个读/写租约。当另一个进程试图截断你正在传输的文件时,内核会向你发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉你内核即将终止你对该文件的写或读租约。在你的程序访问一个无效的地址和被SIGBUS信号杀死之前,你的写调用会被打断了。写入调用的返回值是中断前写入的字节数,errno将被设置为成功。下面是一些示例代码,显示了如何从内核中获得租约。
-if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { - perror("kernel lease set signal"); - return -1; -} -/* l_type can be F_RDLCK F_WRLCK */ -if(fcntl(fd, F_SETLEASE, l_type)){ - perror("kernel lease set type"); - return -1; -}
CommitLog 是 rocketmq 的服务端,也就是 broker 存储消息的的文件,跟 kafka 一样,也是顺序写入,当然消息是变长的,生成的规则是每个文件的默认1G =1024 * 1024 * 1024,commitlog的文件名fileName,名字长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1 073 741 824Byte;当这个文件满了,第二个文件名字为00000000001073741824,起始偏移量为1073741824, 消息存储的时候会顺序写入文件,当文件满了则写入下一个文件,代码中的定义
-// CommitLog file size,default is 1G
-private int mapedFileSizeCommitLog = 1024 * 1024 * 1024;
-
-
本地跑个 demo 验证下,也是这样,这里奇妙有几个比较巧妙的点(个人观点),首先文件就刚好是 1G,并且按照大小偏移量去生成下一个文件,这样获取消息的时候按大小算一下就知道在哪个文件里了,
-代码中写入 CommitLog 的逻辑可以从这开始看
-public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
- // Set the storage time
- msg.setStoreTimestamp(System.currentTimeMillis());
- // Set the message body BODY CRC (consider the most appropriate setting
- // on the client)
- msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
- // Back to Results
- AppendMessageResult result = null;
-
- StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
-
- String topic = msg.getTopic();
- int queueId = msg.getQueueId();
-
- final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
- if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
- || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
- // Delay Delivery
- if (msg.getDelayTimeLevel() > 0) {
- if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
- msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
- }
-
- topic = ScheduleMessageService.SCHEDULE_TOPIC;
- queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
-
- // Backup real topic, queueId
- MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
- MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
- msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
-
- msg.setTopic(topic);
- msg.setQueueId(queueId);
- }
- }
-
- long eclipseTimeInLock = 0;
- MappedFile unlockMappedFile = null;
- MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
-
- putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
- try {
- long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
- this.beginTimeInLock = beginLockTimestamp;
-
- // Here settings are stored timestamp, in order to ensure an orderly
- // global
- msg.setStoreTimestamp(beginLockTimestamp);
-
- if (null == mappedFile || mappedFile.isFull()) {
- mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
- }
- if (null == mappedFile) {
- log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
- beginTimeInLock = 0;
- return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
- }
-
- result = mappedFile.appendMessage(msg, this.appendMessageCallback);
- switch (result.getStatus()) {
- case PUT_OK:
- break;
- case END_OF_FILE:
- unlockMappedFile = mappedFile;
- // Create a new file, re-write the message
- mappedFile = this.mappedFileQueue.getLastMappedFile(0);
- if (null == mappedFile) {
- // XXX: warn and notify me
- log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
- beginTimeInLock = 0;
- return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
- }
- result = mappedFile.appendMessage(msg, this.appendMessageCallback);
- break;
- case MESSAGE_SIZE_EXCEEDED:
- case PROPERTIES_SIZE_EXCEEDED:
- beginTimeInLock = 0;
- return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
- case UNKNOWN_ERROR:
- beginTimeInLock = 0;
- return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
- default:
- beginTimeInLock = 0;
- return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
- }
-
- eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
- beginTimeInLock = 0;
- } finally {
- putMessageLock.unlock();
- }
+ 聊一下 RocketMQ 的消息存储三
+ /2021/10/03/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8%E4%B8%89/
+ ConsumeQueue 其实是定位到一个 topic 下的消息在 CommitLog 下的偏移量,它也是固定大小的
+// ConsumeQueue file size,default is 30W
+private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
- if (eclipseTimeInLock > 500) {
- log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result);
- }
+public static final int CQ_STORE_UNIT_SIZE = 20;
- if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
- this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
- }
+所以文件大小是5.7M 左右
+![5udpag]()
+ConsumeQueue 的构建是通过org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService运行后的 doReput 方法,而启动是的 reputFromOffset 则是通过org.apache.rocketmq.store.DefaultMessageStore#start中下面代码设置并启动
+log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
+ maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
+ this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
+ this.reputMessageService.start();
- PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
+看一下 doReput 的逻辑
+private void doReput() {
+ if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
+ log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
+ this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
+ this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
+ }
+ for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
- // Statistics
- storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
- storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());
+ if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
+ && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
+ break;
+ }
- handleDiskFlush(result, putMessageResult, msg);
- handleHA(result, putMessageResult, msg);
+ // 根据偏移量获取消息
+ SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
+ if (result != null) {
+ try {
+ this.reputFromOffset = result.getStartOffset();
- return putMessageResult;
- }
+ for (int readSize = 0; readSize < result.getSize() && doNext; ) {
+ // 消息校验和转换
+ DispatchRequest dispatchRequest =
+ DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
+ int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
-前面也看到在CommitLog 目录下是有大小为 1G 的文件组成,在实现逻辑中,其实是通过 org.apache.rocketmq.store.MappedFileQueue ,内部是存的一个MappedFile的队列,对于写入的场景每次都是通过org.apache.rocketmq.store.MappedFileQueue#getLastMappedFile() 获取最后一个文件,如果还没有创建,或者最后这个文件已经满了,那就调用 org.apache.rocketmq.store.MappedFileQueue#getLastMappedFile(long)
-public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
- long createOffset = -1;
- // 调用前面的方法,只是从 mappedFileQueue 获取最后一个
- MappedFile mappedFileLast = getLastMappedFile();
+ if (dispatchRequest.isSuccess()) {
+ if (size > 0) {
+ // 进行分发处理,包括 ConsumeQueue 和 IndexFile
+ DefaultMessageStore.this.doDispatch(dispatchRequest);
- // 如果为空,计算下创建的偏移量
- if (mappedFileLast == null) {
- createOffset = startOffset - (startOffset % this.mappedFileSize);
- }
-
- // 如果不为空,但是当前的文件写满了
- if (mappedFileLast != null && mappedFileLast.isFull()) {
- // 前一个的偏移量加上单个文件的偏移量,也就是 1G
- createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
- }
+ if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
+ && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
+ DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
+ dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
+ dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
+ dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
+ }
- if (createOffset != -1 && needCreate) {
- // 根据 createOffset 转换成文件名进行创建
- String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
- String nextNextFilePath = this.storePath + File.separator
- + UtilAll.offset2FileName(createOffset + this.mappedFileSize);
- MappedFile mappedFile = null;
+ this.reputFromOffset += size;
+ readSize += size;
+ if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
+ DefaultMessageStore.this.storeStatsService
+ .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
+ DefaultMessageStore.this.storeStatsService
+ .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
+ .addAndGet(dispatchRequest.getMsgSize());
+ }
+ } else if (size == 0) {
+ this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
+ readSize = result.getSize();
+ }
+ } else if (!dispatchRequest.isSuccess()) {
- // 这里如果allocateMappedFileService 存在,就提交请求
- if (this.allocateMappedFileService != null) {
- mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
- nextNextFilePath, this.mappedFileSize);
- } else {
- try {
- // 否则就直接创建
- mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
- } catch (IOException e) {
- log.error("create mappedFile exception", e);
+ if (size > 0) {
+ log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
+ this.reputFromOffset += size;
+ } else {
+ doNext = false;
+ // If user open the dledger pattern or the broker is master node,
+ // it will not ignore the exception and fix the reputFromOffset variable
+ if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
+ DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
+ log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
+ this.reputFromOffset);
+ this.reputFromOffset += result.getSize() - readSize;
+ }
+ }
+ }
+ }
+ } finally {
+ result.release();
+ }
+ } else {
+ doNext = false;
}
}
+ }
- if (mappedFile != null) {
- if (this.mappedFiles.isEmpty()) {
- mappedFile.setFirstCreateInQueue(true);
- }
- this.mappedFiles.add(mappedFile);
- }
+分发的逻辑看到这
+ class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
- return mappedFile;
+ @Override
+ public void dispatch(DispatchRequest request) {
+ final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
+ switch (tranType) {
+ case MessageSysFlag.TRANSACTION_NOT_TYPE:
+ case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
+ DefaultMessageStore.this.putMessagePositionInfo(request);
+ break;
+ case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
+ case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
+ break;
+ }
}
+ }
+public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
+ ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
+ cq.putMessagePositionInfoWrapper(dispatchRequest);
+ }
- return mappedFileLast;
- }
+真正存储的是在这
+private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
+ final long cqOffset) {
-首先看下直接创建的,
-public MappedFile(final String fileName, final int fileSize) throws IOException {
- init(fileName, fileSize);
+ if (offset + size <= this.maxPhysicOffset) {
+ log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
+ return true;
}
-private void init(final String fileName, final int fileSize) throws IOException {
- this.fileName = fileName;
- this.fileSize = fileSize;
- this.file = new File(fileName);
- this.fileFromOffset = Long.parseLong(this.file.getName());
- boolean ok = false;
- ensureDirOK(this.file.getParent());
+ this.byteBufferIndex.flip();
+ this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
+ this.byteBufferIndex.putLong(offset);
+ this.byteBufferIndex.putInt(size);
+ this.byteBufferIndex.putLong(tagsCode);
+
+这里也可以看到 ConsumeQueue 的存储格式,
+![AA6Tve]()
+偏移量,消息大小,跟 tag 的 hashCode
+]]>public static void main(String[] args) {
+ main0(args);
+ }
+
+ public static NamesrvController main0(String[] args) {
try {
- // 通过 RandomAccessFile 创建 fileChannel
- this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
- // 做 mmap 映射
- this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
- TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
- TOTAL_MAPPED_FILES.incrementAndGet();
- ok = true;
- } catch (FileNotFoundException e) {
- log.error("create file channel " + this.fileName + " Failed. ", e);
- throw e;
- } catch (IOException e) {
- log.error("map file " + this.fileName + " Failed. ", e);
- throw e;
- } finally {
- if (!ok && this.fileChannel != null) {
- this.fileChannel.close();
- }
+ NamesrvController controller = createNamesrvController(args);
+ start(controller);
+ String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
+ log.info(tip);
+ System.out.printf("%s%n", tip);
+ return controller;
+ } catch (Throwable e) {
+ e.printStackTrace();
+ System.exit(-1);
}
- }
-如果是提交给AllocateMappedFileService的话就用到了一些异步操作
public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) {
- int canSubmitRequests = 2;
- if (this.messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
- if (this.messageStore.getMessageStoreConfig().isFastFailIfNoBufferInStorePool()
- && BrokerRole.SLAVE != this.messageStore.getMessageStoreConfig().getBrokerRole()) { //if broker is slave, don't fast fail even no buffer in pool
- canSubmitRequests = this.messageStore.getTransientStorePool().remainBufferNumbs() - this.requestQueue.size();
- }
- }
- // 将请求放在 requestTable 中
- AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize);
- boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null;
- // requestTable 使用了 concurrentHashMap,用文件名作为 key,防止并发
- if (nextPutOK) {
- // 这里判断了是否可以提交到 TransientStorePool,涉及读写分离,后面再细聊
- if (canSubmitRequests <= 0) {
- log.warn("[NOTIFYME]TransientStorePool is not enough, so create mapped file error, " +
- "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().remainBufferNumbs());
- this.requestTable.remove(nextFilePath);
- return null;
- }
- // 塞到阻塞队列中
- boolean offerOK = this.requestQueue.offer(nextReq);
- if (!offerOK) {
- log.warn("never expected here, add a request to preallocate queue failed");
- }
- canSubmitRequests--;
+ return null;
+ }
+
+入口的代码时这样子,其实主要的逻辑在createNamesrvController和start方法,来看下这两个的实现
+public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
+ System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
+ //PackageConflictDetect.detectFastjson();
+
+ Options options = ServerUtil.buildCommandlineOptions(new Options());
+ commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
+ if (null == commandLine) {
+ System.exit(-1);
+ return null;
}
- // 这里的两个提交我猜测是为了多生成一个 CommitLog,
- AllocateRequest nextNextReq = new AllocateRequest(nextNextFilePath, fileSize);
- boolean nextNextPutOK = this.requestTable.putIfAbsent(nextNextFilePath, nextNextReq) == null;
- if (nextNextPutOK) {
- if (canSubmitRequests <= 0) {
- log.warn("[NOTIFYME]TransientStorePool is not enough, so skip preallocate mapped file, " +
- "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().remainBufferNumbs());
- this.requestTable.remove(nextNextFilePath);
- } else {
- boolean offerOK = this.requestQueue.offer(nextNextReq);
- if (!offerOK) {
- log.warn("never expected here, add a request to preallocate queue failed");
- }
+ final NamesrvConfig namesrvConfig = new NamesrvConfig();
+ final NettyServerConfig nettyServerConfig = new NettyServerConfig();
+ nettyServerConfig.setListenPort(9876);
+ if (commandLine.hasOption('c')) {
+ String file = commandLine.getOptionValue('c');
+ if (file != null) {
+ InputStream in = new BufferedInputStream(new FileInputStream(file));
+ properties = new Properties();
+ properties.load(in);
+ MixAll.properties2Object(properties, namesrvConfig);
+ MixAll.properties2Object(properties, nettyServerConfig);
+
+ namesrvConfig.setConfigStorePath(file);
+
+ System.out.printf("load config properties file OK, %s%n", file);
+ in.close();
}
}
- if (hasException) {
- log.warn(this.getServiceName() + " service has exception. so return null");
- return null;
+ if (commandLine.hasOption('p')) {
+ InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
+ MixAll.printObjectProperties(console, namesrvConfig);
+ MixAll.printObjectProperties(console, nettyServerConfig);
+ System.exit(0);
}
- AllocateRequest result = this.requestTable.get(nextFilePath);
- try {
- // 这里就异步等着
- if (result != null) {
- boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);
- if (!waitOK) {
- log.warn("create mmap timeout " + result.getFilePath() + " " + result.getFileSize());
- return null;
- } else {
- this.requestTable.remove(nextFilePath);
- return result.getMappedFile();
- }
- } else {
- log.error("find preallocate mmap failed, this never happen");
- }
- } catch (InterruptedException e) {
- log.warn(this.getServiceName() + " service has exception. ", e);
+ MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
+
+ if (null == namesrvConfig.getRocketmqHome()) {
+ System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
+ System.exit(-2);
}
- return null;
- }
+ LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+ JoranConfigurator configurator = new JoranConfigurator();
+ configurator.setContext(lc);
+ lc.reset();
+ configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
-而真正去执行文件操作的就是 AllocateMappedFileService的 run 方法
public void run() {
- log.info(this.getServiceName() + " service started");
+ log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
- while (!this.isStopped() && this.mmapOperation()) {
+ MixAll.printObjectProperties(log, namesrvConfig);
+ MixAll.printObjectProperties(log, nettyServerConfig);
+
+ final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
+
+ // remember all configs to prevent discard
+ controller.getConfiguration().registerConfig(properties);
+
+ return controller;
+ }
+
+这个方法里其实主要是读取一些配置啥的,不是很复杂,
+public static NamesrvController start(final NamesrvController controller) throws Exception {
+ if (null == controller) {
+ throw new IllegalArgumentException("NamesrvController is null");
}
- log.info(this.getServiceName() + " service end");
- }
-private boolean mmapOperation() {
- boolean isSuccess = false;
- AllocateRequest req = null;
- try {
- // 从阻塞队列里获取请求
- req = this.requestQueue.take();
- AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath());
- if (null == expectedRequest) {
- log.warn("this mmap request expired, maybe cause timeout " + req.getFilePath() + " "
- + req.getFileSize());
- return true;
+
+ boolean initResult = controller.initialize();
+ if (!initResult) {
+ controller.shutdown();
+ System.exit(-3);
+ }
+
+ Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ controller.shutdown();
+ return null;
}
- if (expectedRequest != req) {
- log.warn("never expected here, maybe cause timeout " + req.getFilePath() + " "
- + req.getFileSize() + ", req:" + req + ", expectedRequest:" + expectedRequest);
- return true;
+ }));
+
+ controller.start();
+
+ return controller;
+ }
+
+这个start里主要关注initialize方法,后面就是一个停机的hook,来看下initialize方法
+public boolean initialize() {
+
+ this.kvConfigManager.load();
+
+ this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
+
+ this.remotingExecutor =
+ Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
+
+ this.registerProcessor();
+
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+ @Override
+ public void run() {
+ NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
+ }, 5, 10, TimeUnit.SECONDS);
- if (req.getMappedFile() == null) {
- long beginTime = System.currentTimeMillis();
+ this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
- MappedFile mappedFile;
- if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
- try {
- // 通过 transientStorePool 创建
- mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
- mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
- } catch (RuntimeException e) {
- log.warn("Use default implementation.");
- // 默认创建
- mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
- }
- } else {
- // 默认创建
- mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
- }
+ @Override
+ public void run() {
+ NamesrvController.this.kvConfigManager.printAllPeriodically();
+ }
+ }, 1, 10, TimeUnit.MINUTES);
- long eclipseTime = UtilAll.computeEclipseTimeMilliseconds(beginTime);
- if (eclipseTime > 10) {
- int queueSize = this.requestQueue.size();
- log.warn("create mappedFile spent time(ms) " + eclipseTime + " queue size " + queueSize
- + " " + req.getFilePath() + " " + req.getFileSize());
- }
+ if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
+ // Register a listener to reload SslContext
+ try {
+ fileWatchService = new FileWatchService(
+ new String[] {
+ TlsSystemConfig.tlsServerCertPath,
+ TlsSystemConfig.tlsServerKeyPath,
+ TlsSystemConfig.tlsServerTrustCertPath
+ },
+ new FileWatchService.Listener() {
+ boolean certChanged, keyChanged = false;
+ @Override
+ public void onChanged(String path) {
+ if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
+ log.info("The trust certificate changed, reload the ssl context");
+ reloadServerSslContext();
+ }
+ if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
+ certChanged = true;
+ }
+ if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
+ keyChanged = true;
+ }
+ if (certChanged && keyChanged) {
+ log.info("The certificate and private key changed, reload the ssl context");
+ certChanged = keyChanged = false;
+ reloadServerSslContext();
+ }
+ }
+ private void reloadServerSslContext() {
+ ((NettyRemotingServer) remotingServer).loadSslContext();
+ }
+ });
+ } catch (Exception e) {
+ log.warn("FileWatchService created error, can't load the certificate dynamically");
+ }
+ }
- // pre write mappedFile
- if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
- .getMapedFileSizeCommitLog()
- &&
- this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
- mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
- this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
- }
+ return true;
+ }
- req.setMappedFile(mappedFile);
- this.hasException = false;
- isSuccess = true;
+这里的kvConfigManager主要是来加载NameServer的配置参数,存到org.apache.rocketmq.namesrv.kvconfig.KVConfigManager#configTable中,然后是以BrokerHousekeepingService对象为参数初始化NettyRemotingServer对象,BrokerHousekeepingService对象作为该Netty连接中Socket链接的监听器(ChannelEventListener);监听与Broker建立的渠道的状态(空闲、关闭、异常三个状态),并调用BrokerHousekeepingService的相应onChannel方法。其中渠道的空闲、关闭、异常状态均调用RouteInfoManager.onChannelDestory方法处理。这个BrokerHousekeepingService可以字面化地理解为broker的管家服务,这个类内部三个状态方法其实都是调用的org.apache.rocketmq.namesrv.NamesrvController#getRouteInfoManager方法,而这个RouteInfoManager里面的对象有这些
+public class RouteInfoManager {
+ private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
+ private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ // topic与queue的对应关系
+ private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
+ // Broker名称与broker属性的map
+ private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
+ // 集群与broker集合的对应关系
+ private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
+ // 活跃的broker信息
+ private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
+ // Broker地址与过滤器
+ private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
+
+然后接下去就是初始化了一个线程池,然后注册默认的处理类this.registerProcessor();默认都是这个处理器去处理请求 org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#DefaultRequestProcessor然后是初始化两个定时任务
第一是每10秒检查一遍Broker的状态的定时任务,调用scanNotActiveBroker方法;遍历brokerLiveTable集合,查看每个broker的最后更新时间(BrokerLiveInfo.lastUpdateTimestamp)是否超过2分钟,若超过则关闭该broker的渠道并调用RouteInfoManager.onChannelDestory方法清理RouteInfoManager类的topicQueueTable、brokerAddrTable、clusterAddrTable、filterServerTable成员变量。
+this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+ @Override
+ public void run() {
+ NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
- } catch (InterruptedException e) {
- log.warn(this.getServiceName() + " interrupted, possibly by shutdown.");
- this.hasException = true;
- return false;
- } catch (IOException e) {
- log.warn(this.getServiceName() + " service has exception. ", e);
- this.hasException = true;
- if (null != req) {
- requestQueue.offer(req);
+ }, 5, 10, TimeUnit.SECONDS);
+public void scanNotActiveBroker() {
+ Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, BrokerLiveInfo> next = it.next();
+ long last = next.getValue().getLastUpdateTimestamp();
+ if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
+ RemotingUtil.closeChannel(next.getValue().getChannel());
+ it.remove();
+ log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
+ this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
+ }
+ }
+ }
+
+ public void onChannelDestroy(String remoteAddr, Channel channel) {
+ String brokerAddrFound = null;
+ if (channel != null) {
+ try {
try {
- Thread.sleep(1);
- } catch (InterruptedException ignored) {
+ this.lock.readLock().lockInterruptibly();
+ Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
+ this.brokerLiveTable.entrySet().iterator();
+ while (itBrokerLiveTable.hasNext()) {
+ Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
+ if (entry.getValue().getChannel() == channel) {
+ brokerAddrFound = entry.getKey();
+ break;
+ }
+ }
+ } finally {
+ this.lock.readLock().unlock();
}
+ } catch (Exception e) {
+ log.error("onChannelDestroy Exception", e);
}
- } finally {
- if (req != null && isSuccess)
- // 通知前面等待的
- req.getCountDownLatch().countDown();
}
- return true;
- }
-
-
+ if (null == brokerAddrFound) {
+ brokerAddrFound = remoteAddr;
+ } else {
+ log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
+ }
-]]>// ConsumeQueue file size,default is 30W
-private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE;
-
-public static final int CQ_STORE_UNIT_SIZE = 20;
+ if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
-所以文件大小是5.7M 左右
-
ConsumeQueue 的构建是通过org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService运行后的 doReput 方法,而启动是的 reputFromOffset 则是通过org.apache.rocketmq.store.DefaultMessageStore#start中下面代码设置并启动
log.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}",
- maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset());
- this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
- this.reputMessageService.start();
+ try {
+ try {
+ this.lock.writeLock().lockInterruptibly();
+ this.brokerLiveTable.remove(brokerAddrFound);
+ this.filterServerTable.remove(brokerAddrFound);
+ String brokerNameFound = null;
+ boolean removeBrokerName = false;
+ Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
+ this.brokerAddrTable.entrySet().iterator();
+ while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
+ BrokerData brokerData = itBrokerAddrTable.next().getValue();
-看一下 doReput 的逻辑
-private void doReput() {
- if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
- log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
- this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
- this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
- }
- for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
+ Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<Long, String> entry = it.next();
+ Long brokerId = entry.getKey();
+ String brokerAddr = entry.getValue();
+ if (brokerAddr.equals(brokerAddrFound)) {
+ brokerNameFound = brokerData.getBrokerName();
+ it.remove();
+ log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
+ brokerId, brokerAddr);
+ break;
+ }
+ }
- if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
- && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
- break;
- }
+ if (brokerData.getBrokerAddrs().isEmpty()) {
+ removeBrokerName = true;
+ itBrokerAddrTable.remove();
+ log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
+ brokerData.getBrokerName());
+ }
+ }
- // 根据偏移量获取消息
- SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
- if (result != null) {
- try {
- this.reputFromOffset = result.getStartOffset();
+ if (brokerNameFound != null && removeBrokerName) {
+ Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, Set<String>> entry = it.next();
+ String clusterName = entry.getKey();
+ Set<String> brokerNames = entry.getValue();
+ boolean removed = brokerNames.remove(brokerNameFound);
+ if (removed) {
+ log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
+ brokerNameFound, clusterName);
- for (int readSize = 0; readSize < result.getSize() && doNext; ) {
- // 消息校验和转换
- DispatchRequest dispatchRequest =
- DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
- int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
+ if (brokerNames.isEmpty()) {
+ log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
+ clusterName);
+ it.remove();
+ }
- if (dispatchRequest.isSuccess()) {
- if (size > 0) {
- // 进行分发处理,包括 ConsumeQueue 和 IndexFile
- DefaultMessageStore.this.doDispatch(dispatchRequest);
+ break;
+ }
+ }
+ }
- if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
- && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
- DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
- dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
- dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
- dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
- }
+ if (removeBrokerName) {
+ Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
+ this.topicQueueTable.entrySet().iterator();
+ while (itTopicQueueTable.hasNext()) {
+ Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
+ String topic = entry.getKey();
+ List<QueueData> queueDataList = entry.getValue();
- this.reputFromOffset += size;
- readSize += size;
- if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
- DefaultMessageStore.this.storeStatsService
- .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
- DefaultMessageStore.this.storeStatsService
- .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
- .addAndGet(dispatchRequest.getMsgSize());
- }
- } else if (size == 0) {
- this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
- readSize = result.getSize();
+ Iterator<QueueData> itQueueData = queueDataList.iterator();
+ while (itQueueData.hasNext()) {
+ QueueData queueData = itQueueData.next();
+ if (queueData.getBrokerName().equals(brokerNameFound)) {
+ itQueueData.remove();
+ log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
+ topic, queueData);
}
- } else if (!dispatchRequest.isSuccess()) {
+ }
- if (size > 0) {
- log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
- this.reputFromOffset += size;
- } else {
- doNext = false;
- // If user open the dledger pattern or the broker is master node,
- // it will not ignore the exception and fix the reputFromOffset variable
- if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
- DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
- log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
- this.reputFromOffset);
- this.reputFromOffset += result.getSize() - readSize;
- }
- }
+ if (queueDataList.isEmpty()) {
+ itTopicQueueTable.remove();
+ log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
+ topic);
}
}
- } finally {
- result.release();
}
+ } finally {
+ this.lock.writeLock().unlock();
+ }
+ } catch (Exception e) {
+ log.error("onChannelDestroy Exception", e);
+ }
+ }
+ }
+
+第二个是每10分钟打印一次NameServer的配置参数。即KVConfigManager.configTable变量的内容。
+this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+
+ @Override
+ public void run() {
+ NamesrvController.this.kvConfigManager.printAllPeriodically();
+ }
+ }, 1, 10, TimeUnit.MINUTES);
+
+然后这个初始化就差不多完成了,后面只需要把remotingServer start一下就好了
+直接上代码,其实主体是swtich case去判断
+@Override
+ public RemotingCommand processRequest(ChannelHandlerContext ctx,
+ RemotingCommand request) throws RemotingCommandException {
+
+ if (ctx != null) {
+ log.debug("receive request, {} {} {}",
+ request.getCode(),
+ RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
+ request);
+ }
+
+
+ switch (request.getCode()) {
+ case RequestCode.PUT_KV_CONFIG:
+ return this.putKVConfig(ctx, request);
+ case RequestCode.GET_KV_CONFIG:
+ return this.getKVConfig(ctx, request);
+ case RequestCode.DELETE_KV_CONFIG:
+ return this.deleteKVConfig(ctx, request);
+ case RequestCode.QUERY_DATA_VERSION:
+ return queryBrokerTopicConfig(ctx, request);
+ case RequestCode.REGISTER_BROKER:
+ Version brokerVersion = MQVersion.value2Version(request.getVersion());
+ if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
+ return this.registerBrokerWithFilterServer(ctx, request);
+ } else {
+ return this.registerBroker(ctx, request);
+ }
+ case RequestCode.UNREGISTER_BROKER:
+ return this.unregisterBroker(ctx, request);
+ case RequestCode.GET_ROUTEINTO_BY_TOPIC:
+ return this.getRouteInfoByTopic(ctx, request);
+ case RequestCode.GET_BROKER_CLUSTER_INFO:
+ return this.getBrokerClusterInfo(ctx, request);
+ case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
+ return this.wipeWritePermOfBroker(ctx, request);
+ case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
+ return getAllTopicListFromNameserver(ctx, request);
+ case RequestCode.DELETE_TOPIC_IN_NAMESRV:
+ return deleteTopicInNamesrv(ctx, request);
+ case RequestCode.GET_KVLIST_BY_NAMESPACE:
+ return this.getKVListByNamespace(ctx, request);
+ case RequestCode.GET_TOPICS_BY_CLUSTER:
+ return this.getTopicsByCluster(ctx, request);
+ case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
+ return this.getSystemTopicListFromNs(ctx, request);
+ case RequestCode.GET_UNIT_TOPIC_LIST:
+ return this.getUnitTopicList(ctx, request);
+ case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
+ return this.getHasUnitSubTopicList(ctx, request);
+ case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
+ return this.getHasUnitSubUnUnitTopicList(ctx, request);
+ case RequestCode.UPDATE_NAMESRV_CONFIG:
+ return this.updateConfig(ctx, request);
+ case RequestCode.GET_NAMESRV_CONFIG:
+ return this.getConfig(ctx, request);
+ default:
+ break;
+ }
+ return null;
+ }
+
+以broker注册为例,
+case RequestCode.REGISTER_BROKER:
+ Version brokerVersion = MQVersion.value2Version(request.getVersion());
+ if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
+ return this.registerBrokerWithFilterServer(ctx, request);
} else {
- doNext = false;
- }
- }
- }
+ return this.registerBroker(ctx, request);
+ }
-分发的逻辑看到这
- class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
+做了个简单的版本管理,我们看下前面一个的代码
+public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
+ throws RemotingCommandException {
+ final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
+ final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
+ final RegisterBrokerRequestHeader requestHeader =
+ (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);
- @Override
- public void dispatch(DispatchRequest request) {
- final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
- switch (tranType) {
- case MessageSysFlag.TRANSACTION_NOT_TYPE:
- case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
- DefaultMessageStore.this.putMessagePositionInfo(request);
- break;
- case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
- case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
- break;
- }
- }
+ if (!checksum(ctx, request, requestHeader)) {
+ response.setCode(ResponseCode.SYSTEM_ERROR);
+ response.setRemark("crc32 not match");
+ return response;
}
-public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
- ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
- cq.putMessagePositionInfoWrapper(dispatchRequest);
- }
-真正存储的是在这
-private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
- final long cqOffset) {
+ RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
- if (offset + size <= this.maxPhysicOffset) {
- log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
- return true;
+ if (request.getBody() != null) {
+ try {
+ registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
+ } catch (Exception e) {
+ throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
+ }
+ } else {
+ registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
+ registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
}
- this.byteBufferIndex.flip();
- this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
- this.byteBufferIndex.putLong(offset);
- this.byteBufferIndex.putInt(size);
- this.byteBufferIndex.putLong(tagsCode);
+ RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
+ requestHeader.getClusterName(),
+ requestHeader.getBrokerAddr(),
+ requestHeader.getBrokerName(),
+ requestHeader.getBrokerId(),
+ requestHeader.getHaServerAddr(),
+ registerBrokerBody.getTopicConfigSerializeWrapper(),
+ registerBrokerBody.getFilterServerList(),
+ ctx.channel());
-这里也可以看到 ConsumeQueue 的存储格式,
-![AA6Tve]()
-偏移量,消息大小,跟 tag 的 hashCode
-]]>IndexFile 的构建则是分发给这个进行处理
-class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
+ responseHeader.setHaServerAddr(result.getHaServerAddr());
+ responseHeader.setMasterAddr(result.getMasterAddr());
- @Override
- public void dispatch(DispatchRequest request) {
- if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
- DefaultMessageStore.this.indexService.buildIndex(request);
- }
- }
-}
-public void buildIndex(DispatchRequest req) {
- IndexFile indexFile = retryGetAndCreateIndexFile();
- if (indexFile != null) {
- long endPhyOffset = indexFile.getEndPhyOffset();
- DispatchRequest msg = req;
- String topic = msg.getTopic();
- String keys = msg.getKeys();
- if (msg.getCommitLogOffset() < endPhyOffset) {
- return;
- }
+ byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
+ response.setBody(jsonValue);
- final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
- switch (tranType) {
- case MessageSysFlag.TRANSACTION_NOT_TYPE:
- case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
- case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
- break;
- case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
- return;
- }
+ response.setCode(ResponseCode.SUCCESS);
+ response.setRemark(null);
+ return response;
+}
- if (req.getUniqKey() != null) {
- indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
- if (indexFile == null) {
- log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
- return;
+可以看到主要的逻辑还是在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker这个方法里
public RegisterBrokerResult registerBroker(
+ final String clusterName,
+ final String brokerAddr,
+ final String brokerName,
+ final long brokerId,
+ final String haServerAddr,
+ final TopicConfigSerializeWrapper topicConfigWrapper,
+ final List<String> filterServerList,
+ final Channel channel) {
+ RegisterBrokerResult result = new RegisterBrokerResult();
+ try {
+ try {
+ this.lock.writeLock().lockInterruptibly();
+
+ // 更新这个clusterAddrTable
+ Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
+ if (null == brokerNames) {
+ brokerNames = new HashSet<String>();
+ this.clusterAddrTable.put(clusterName, brokerNames);
}
- }
+ brokerNames.add(brokerName);
- if (keys != null && keys.length() > 0) {
- String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
- for (int i = 0; i < keyset.length; i++) {
- String key = keyset[i];
- if (key.length() > 0) {
- indexFile = putKey(indexFile, msg, buildKey(topic, key));
- if (indexFile == null) {
- log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
- return;
- }
+ boolean registerFirst = false;
+
+ // 更新brokerAddrTable
+ BrokerData brokerData = this.brokerAddrTable.get(brokerName);
+ if (null == brokerData) {
+ registerFirst = true;
+ brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
+ this.brokerAddrTable.put(brokerName, brokerData);
+ }
+ Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
+ //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
+ //The same IP:PORT must only have one record in brokerAddrTable
+ Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<Long, String> item = it.next();
+ if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
+ it.remove();
}
}
- }
- } else {
- log.error("build index error, stop building index");
- }
- }
-
-配置的数量
-private boolean messageIndexEnable = true;
-private int maxHashSlotNum = 5000000;
-private int maxIndexNum = 5000000 * 4;
-最核心的其实是 IndexFile 的结构和如何写入
-public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
- if (this.indexHeader.getIndexCount() < this.indexNum) {
- // 获取 key 的 hash
- int keyHash = indexKeyHashMethod(key);
- // 计算属于哪个 slot
- int slotPos = keyHash % this.hashSlotNum;
- // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
- int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
+ String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
+ registerFirst = registerFirst || (null == oldAddr);
- FileLock fileLock = null;
+ // 更新了org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable中的数据
+ if (null != topicConfigWrapper
+ && MixAll.MASTER_ID == brokerId) {
+ if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
+ || registerFirst) {
+ ConcurrentMap<String, TopicConfig> tcTable =
+ topicConfigWrapper.getTopicConfigTable();
+ if (tcTable != null) {
+ for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
+ this.createAndUpdateQueueData(brokerName, entry.getValue());
+ }
+ }
+ }
+ }
- try {
+ // 更新活跃broker信息
+ BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
+ new BrokerLiveInfo(
+ System.currentTimeMillis(),
+ topicConfigWrapper.getDataVersion(),
+ channel,
+ haServerAddr));
+ if (null == prevBrokerLiveInfo) {
+ log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
+ }
- // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
- // false);
- int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
- if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
- slotValue = invalidIndex;
+ // 处理filter
+ if (filterServerList != null) {
+ if (filterServerList.isEmpty()) {
+ this.filterServerTable.remove(brokerAddr);
+ } else {
+ this.filterServerTable.put(brokerAddr, filterServerList);
+ }
}
- long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
-
- timeDiff = timeDiff / 1000;
-
- if (this.indexHeader.getBeginTimestamp() <= 0) {
- timeDiff = 0;
- } else if (timeDiff > Integer.MAX_VALUE) {
- timeDiff = Integer.MAX_VALUE;
- } else if (timeDiff < 0) {
- timeDiff = 0;
+ // 当当前broker非master时返回master信息
+ if (MixAll.MASTER_ID != brokerId) {
+ String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
+ if (masterAddr != null) {
+ BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
+ if (brokerLiveInfo != null) {
+ result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
+ result.setMasterAddr(masterAddr);
+ }
+ }
}
+ } finally {
+ this.lock.writeLock().unlock();
+ }
+ } catch (Exception e) {
+ log.error("registerBroker Exception", e);
+ }
- // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
- int absIndexPos =
- IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
- + this.indexHeader.getIndexCount() * indexSize;
-
- this.mappedByteBuffer.putInt(absIndexPos, keyHash);
- this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
- this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
- this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
-
- // 存放的是数量位移,不是绝对位置
- this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
+ return result;
+ }
- if (this.indexHeader.getIndexCount() <= 1) {
- this.indexHeader.setBeginPhyOffset(phyOffset);
- this.indexHeader.setBeginTimestamp(storeTimestamp);
- }
+这个是注册 broker 的逻辑,再看下根据 topic 获取 broker 信息和 topic 信息,org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic 主要是这个方法的逻辑
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
+ RemotingCommand request) throws RemotingCommandException {
+ final RemotingCommand response = RemotingCommand.createResponseCommand(null);
+ final GetRouteInfoRequestHeader requestHeader =
+ (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
- this.indexHeader.incHashSlotCount();
- this.indexHeader.incIndexCount();
- this.indexHeader.setEndPhyOffset(phyOffset);
- this.indexHeader.setEndTimestamp(storeTimestamp);
+ TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
- return true;
- } catch (Exception e) {
- log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
- } finally {
- if (fileLock != null) {
- try {
- fileLock.release();
- } catch (IOException e) {
- log.error("Failed to release the lock", e);
- }
- }
+ if (topicRouteData != null) {
+ if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
+ String orderTopicConf =
+ this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
+ requestHeader.getTopic());
+ topicRouteData.setOrderTopicConf(orderTopicConf);
}
- } else {
- log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
- + "; index max num = " + this.indexNum);
+
+ byte[] content = topicRouteData.encode();
+ response.setBody(content);
+ response.setCode(ResponseCode.SUCCESS);
+ response.setRemark(null);
+ return response;
}
- return false;
- }
+ response.setCode(ResponseCode.TOPIC_NOT_EXIST);
+ response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
+ + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
+ return response;
+ }
-具体可以看一下这个简略的示意图
首先调用org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#pickupTopicRouteData从org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#topicQueueTable获取到org.apache.rocketmq.common.protocol.route.QueueData这里面存了 brokerName,再通过org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#brokerAddrTable里获取到 broker 的地址信息等,然后再获取 orderMessage 的配置。
简要分析了下 RocketMQ 的 NameServer 的代码,比较粗粒度。
]]>首先这里的场景跟我原来用的有点点区别,在项目中使用的是通过配置中心控制数据源切换,统一切换,而这里的例子多加了个可以根据接口注解配置
-第一部分是最核心的,如何基于 Spring JDBC 和 Druid 来实现数据源切换,是继承了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 这个类,他的determineCurrentLookupKey方法会被调用来获得用来决定选择那个数据源的对象,也就是 lookupKey,也可以通过这个类看到就是通过这个 lookupKey 来路由找到数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
+ 介绍下最近比较实用的端口转发
+ /2021/11/14/%E4%BB%8B%E7%BB%8D%E4%B8%8B%E6%9C%80%E8%BF%91%E6%AF%94%E8%BE%83%E5%AE%9E%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/
+ vscode 扩展转发在日常使用云服务器的时候,如果要访问上面自建的 mysql,一般要不直接开对应的端口,然后需要对本地 ip 进行授权,但是这个方案会有比较多的限制,比如本地 ip 变了,比如是非固定出口 ip 的家用宽带,或者要在家里跟公司都要访问,如果对所有 ip 都授权的话会不安全,这个时候其实是用 ssh 端口转发是个比较安全方便的方式。
原来在这个之前其实对这块内容不太了解,后面是听朋友说的,vscode 的 Remote - SSH 扩展可以很方便的使用端口转发,在使用该扩展的时候,会在控制台位置里都出现一个”端口” tab
![]()
如图中所示,我就是将一个服务器上的 mysql 的 3306 端口转发到本地的 3307 端口,至于为什么不用 3306 是因为本地我也有个 mysql 已经使用了 3306 端口,这个方法是使用的 vscode 的这个扩展,
+ssh 命令转发
还有个方式是直接使用 ssh 命令
命令可以如此
+ssh -CfNg -L 3307:127.0.0.1:3306 user1@199.199.199.199
+简单介绍下这个命令
-C 表示的是压缩数据包
-f 表示后台执行命令
-N 是表示不执行具体命令只用于端口转发
-g 表示允许远程主机连接本地转发端口
-L 则是具体端口转发的映射配置
上面的命令就是将远程主机的 127.0.0.1:3306 对应转发到本地 3307
而后面的用户则就是登录主机的用户名user1和ip地址199.199.199.199,当然这个配置也不是唯一的
+ssh config 配置转发
还可以在ssh 的 config 配置中加对应的配置
+Host host1
+ HostName 199.199.199.199
+ User user1
+ IdentityFile /Users/user1/.ssh/id_rsa
+ ServerAliveInterval 60
+ LocalForward 3310 127.0.0.1:3306
+然后通过 ssh host1 连接服务器的时候就能顺带做端口转发
+]]>
+
+ ssh
+ 技巧
+
+
+ ssh
+ 端口转发
+
+ CommitLog 是 rocketmq 的服务端,也就是 broker 存储消息的的文件,跟 kafka 一样,也是顺序写入,当然消息是变长的,生成的规则是每个文件的默认1G =1024 * 1024 * 1024,commitlog的文件名fileName,名字长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1 073 741 824Byte;当这个文件满了,第二个文件名字为00000000001073741824,起始偏移量为1073741824, 消息存储的时候会顺序写入文件,当文件满了则写入下一个文件,代码中的定义
+// CommitLog file size,default is 1G
+private int mapedFileSizeCommitLog = 1024 * 1024 * 1024;
- @Override
- protected Object determineCurrentLookupKey() {
- if (DatabaseContextHolder.getDatabaseType() != null) {
- return DatabaseContextHolder.getDatabaseType().getName();
+
本地跑个 demo 验证下,也是这样,这里奇妙有几个比较巧妙的点(个人观点),首先文件就刚好是 1G,并且按照大小偏移量去生成下一个文件,这样获取消息的时候按大小算一下就知道在哪个文件里了,
+代码中写入 CommitLog 的逻辑可以从这开始看
+public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
+ // Set the storage time
+ msg.setStoreTimestamp(System.currentTimeMillis());
+ // Set the message body BODY CRC (consider the most appropriate setting
+ // on the client)
+ msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
+ // Back to Results
+ AppendMessageResult result = null;
+
+ StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
+
+ String topic = msg.getTopic();
+ int queueId = msg.getQueueId();
+
+ final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
+ if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
+ || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
+ // Delay Delivery
+ if (msg.getDelayTimeLevel() > 0) {
+ if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
+ msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
+ }
+
+ topic = ScheduleMessageService.SCHEDULE_TOPIC;
+ queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
+
+ // Backup real topic, queueId
+ MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
+ MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
+ msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
+
+ msg.setTopic(topic);
+ msg.setQueueId(queueId);
+ }
+ }
+
+ long eclipseTimeInLock = 0;
+ MappedFile unlockMappedFile = null;
+ MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
+
+ putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
+ try {
+ long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
+ this.beginTimeInLock = beginLockTimestamp;
+
+ // Here settings are stored timestamp, in order to ensure an orderly
+ // global
+ msg.setStoreTimestamp(beginLockTimestamp);
+
+ if (null == mappedFile || mappedFile.isFull()) {
+ mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
+ }
+ if (null == mappedFile) {
+ log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
+ beginTimeInLock = 0;
+ return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
+ }
+
+ result = mappedFile.appendMessage(msg, this.appendMessageCallback);
+ switch (result.getStatus()) {
+ case PUT_OK:
+ break;
+ case END_OF_FILE:
+ unlockMappedFile = mappedFile;
+ // Create a new file, re-write the message
+ mappedFile = this.mappedFileQueue.getLastMappedFile(0);
+ if (null == mappedFile) {
+ // XXX: warn and notify me
+ log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
+ beginTimeInLock = 0;
+ return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
+ }
+ result = mappedFile.appendMessage(msg, this.appendMessageCallback);
+ break;
+ case MESSAGE_SIZE_EXCEEDED:
+ case PROPERTIES_SIZE_EXCEEDED:
+ beginTimeInLock = 0;
+ return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
+ case UNKNOWN_ERROR:
+ beginTimeInLock = 0;
+ return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
+ default:
+ beginTimeInLock = 0;
+ return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
+ }
+
+ eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
+ beginTimeInLock = 0;
+ } finally {
+ putMessageLock.unlock();
+ }
+
+ if (eclipseTimeInLock > 500) {
+ log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result);
+ }
+
+ if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
+ this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
}
- return DatabaseType.MASTER1.getName();
- }
-}
-而如何使用这个 lookupKey 呢,就涉及到我们的 DataSource 配置了,原来就是我们可以直接通过spring 的 jdbc 配置数据源,像这样
-
现在我们要使用 Druid 作为数据源了,然后配置 DynamicDataSource 的参数,通过 key 来选择对应的 DataSource,也就是下面配的 master1 和 master2
<bean id="master1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
- destroy-method="close"
- p:driverClassName="com.mysql.cj.jdbc.Driver"
- p:url="${master1.demo.datasource.url}"
- p:username="${master1.demo.datasource.username}"
- p:password="${master1.demo.datasource.password}"
- p:initialSize="5"
- p:minIdle="1"
- p:maxActive="10"
- p:maxWait="60000"
- p:timeBetweenEvictionRunsMillis="60000"
- p:minEvictableIdleTimeMillis="300000"
- p:validationQuery="SELECT 'x'"
- p:testWhileIdle="true"
- p:testOnBorrow="false"
- p:testOnReturn="false"
- p:poolPreparedStatements="false"
- p:maxPoolPreparedStatementPerConnectionSize="20"
- p:connectionProperties="config.decrypt=true"
- p:filters="stat,config"/>
+ PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
- <bean id="master2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
- destroy-method="close"
- p:driverClassName="com.mysql.cj.jdbc.Driver"
- p:url="${master2.demo.datasource.url}"
- p:username="${master2.demo.datasource.username}"
- p:password="${master2.demo.datasource.password}"
- p:initialSize="5"
- p:minIdle="1"
- p:maxActive="10"
- p:maxWait="60000"
- p:timeBetweenEvictionRunsMillis="60000"
- p:minEvictableIdleTimeMillis="300000"
- p:validationQuery="SELECT 'x'"
- p:testWhileIdle="true"
- p:testOnBorrow="false"
- p:testOnReturn="false"
- p:poolPreparedStatements="false"
- p:maxPoolPreparedStatementPerConnectionSize="20"
- p:connectionProperties="config.decrypt=true"
- p:filters="stat,config"/>
+ // Statistics
+ storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
+ storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());
- <bean id="dataSource" class="com.nicksxs.springdemo.config.DynamicDataSource">
- <property name="targetDataSources">
- <map key-type="java.lang.String">
- <!-- master -->
- <entry key="master1" value-ref="master1"/>
- <!-- slave -->
- <entry key="master2" value-ref="master2"/>
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="master1"/>
- </bean>
+ handleDiskFlush(result, putMessageResult, msg);
+ handleHA(result, putMessageResult, msg);
-现在就要回到头上,介绍下这个DatabaseContextHolder,这里使用了 ThreadLocal 存放这个 DatabaseType,为啥要用这个是因为前面说的我们想要让接口层面去配置不同的数据源,要把持相互隔离不受影响,就使用了 ThreadLocal,关于它也可以看我前面写的一篇文章聊聊传说中的 ThreadLocal,而 DatabaseType 就是个简单的枚举
public class DatabaseContextHolder {
- public static final ThreadLocal<DatabaseType> databaseTypeThreadLocal = new ThreadLocal<>();
+ return putMessageResult;
+ }
- public static DatabaseType getDatabaseType() {
- return databaseTypeThreadLocal.get();
- }
+前面也看到在CommitLog 目录下是有大小为 1G 的文件组成,在实现逻辑中,其实是通过 org.apache.rocketmq.store.MappedFileQueue ,内部是存的一个MappedFile的队列,对于写入的场景每次都是通过org.apache.rocketmq.store.MappedFileQueue#getLastMappedFile() 获取最后一个文件,如果还没有创建,或者最后这个文件已经满了,那就调用 org.apache.rocketmq.store.MappedFileQueue#getLastMappedFile(long)
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
+ long createOffset = -1;
+ // 调用前面的方法,只是从 mappedFileQueue 获取最后一个
+ MappedFile mappedFileLast = getLastMappedFile();
- public static void putDatabaseType(DatabaseType databaseType) {
- databaseTypeThreadLocal.set(databaseType);
- }
+ // 如果为空,计算下创建的偏移量
+ if (mappedFileLast == null) {
+ createOffset = startOffset - (startOffset % this.mappedFileSize);
+ }
+
+ // 如果不为空,但是当前的文件写满了
+ if (mappedFileLast != null && mappedFileLast.isFull()) {
+ // 前一个的偏移量加上单个文件的偏移量,也就是 1G
+ createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
+ }
- public static void clearDatabaseType() {
- databaseTypeThreadLocal.remove();
- }
-}
-public enum DatabaseType {
- MASTER1("master1", "1"),
- MASTER2("master2", "2");
+ if (createOffset != -1 && needCreate) {
+ // 根据 createOffset 转换成文件名进行创建
+ String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
+ String nextNextFilePath = this.storePath + File.separator
+ + UtilAll.offset2FileName(createOffset + this.mappedFileSize);
+ MappedFile mappedFile = null;
- private final String name;
- private final String value;
+ // 这里如果allocateMappedFileService 存在,就提交请求
+ if (this.allocateMappedFileService != null) {
+ mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
+ nextNextFilePath, this.mappedFileSize);
+ } else {
+ try {
+ // 否则就直接创建
+ mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
+ } catch (IOException e) {
+ log.error("create mappedFile exception", e);
+ }
+ }
- DatabaseType(String name, String value) {
- this.name = name;
- this.value = value;
- }
+ if (mappedFile != null) {
+ if (this.mappedFiles.isEmpty()) {
+ mappedFile.setFirstCreateInQueue(true);
+ }
+ this.mappedFiles.add(mappedFile);
+ }
- public String getName() {
- return name;
- }
+ return mappedFile;
+ }
- public String getValue() {
- return value;
- }
+ return mappedFileLast;
+ }
- public static DatabaseType getDatabaseType(String name) {
- if (MASTER2.name.equals(name)) {
- return MASTER2;
- }
- return MASTER1;
+首先看下直接创建的,
+public MappedFile(final String fileName, final int fileSize) throws IOException {
+ init(fileName, fileSize);
}
-}
+private void init(final String fileName, final int fileSize) throws IOException {
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ this.file = new File(fileName);
+ this.fileFromOffset = Long.parseLong(this.file.getName());
+ boolean ok = false;
-这边可以看到就是通过动态地通过putDatabaseType设置lookupKey来进行数据源切换,要通过接口注解配置来进行设置的话,我们就需要一个注解
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface DataSource {
- String value();
-}
+ ensureDirOK(this.file.getParent());
-这个注解可以配置在我的接口方法上,比如这样
-public interface StudentService {
+ try {
+ // 通过 RandomAccessFile 创建 fileChannel
+ this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
+ // 做 mmap 映射
+ this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
+ TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
+ TOTAL_MAPPED_FILES.incrementAndGet();
+ ok = true;
+ } catch (FileNotFoundException e) {
+ log.error("create file channel " + this.fileName + " Failed. ", e);
+ throw e;
+ } catch (IOException e) {
+ log.error("map file " + this.fileName + " Failed. ", e);
+ throw e;
+ } finally {
+ if (!ok && this.fileChannel != null) {
+ this.fileChannel.close();
+ }
+ }
+ }
- @DataSource("master1")
- public Student queryOne();
+如果是提交给AllocateMappedFileService的话就用到了一些异步操作
public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) {
+ int canSubmitRequests = 2;
+ if (this.messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
+ if (this.messageStore.getMessageStoreConfig().isFastFailIfNoBufferInStorePool()
+ && BrokerRole.SLAVE != this.messageStore.getMessageStoreConfig().getBrokerRole()) { //if broker is slave, don't fast fail even no buffer in pool
+ canSubmitRequests = this.messageStore.getTransientStorePool().remainBufferNumbs() - this.requestQueue.size();
+ }
+ }
+ // 将请求放在 requestTable 中
+ AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize);
+ boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null;
+ // requestTable 使用了 concurrentHashMap,用文件名作为 key,防止并发
+ if (nextPutOK) {
+ // 这里判断了是否可以提交到 TransientStorePool,涉及读写分离,后面再细聊
+ if (canSubmitRequests <= 0) {
+ log.warn("[NOTIFYME]TransientStorePool is not enough, so create mapped file error, " +
+ "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().remainBufferNumbs());
+ this.requestTable.remove(nextFilePath);
+ return null;
+ }
+ // 塞到阻塞队列中
+ boolean offerOK = this.requestQueue.offer(nextReq);
+ if (!offerOK) {
+ log.warn("never expected here, add a request to preallocate queue failed");
+ }
+ canSubmitRequests--;
+ }
- @DataSource("master2")
- public Student queryAnother();
+ // 这里的两个提交我猜测是为了多生成一个 CommitLog,
+ AllocateRequest nextNextReq = new AllocateRequest(nextNextFilePath, fileSize);
+ boolean nextNextPutOK = this.requestTable.putIfAbsent(nextNextFilePath, nextNextReq) == null;
+ if (nextNextPutOK) {
+ if (canSubmitRequests <= 0) {
+ log.warn("[NOTIFYME]TransientStorePool is not enough, so skip preallocate mapped file, " +
+ "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().remainBufferNumbs());
+ this.requestTable.remove(nextNextFilePath);
+ } else {
+ boolean offerOK = this.requestQueue.offer(nextNextReq);
+ if (!offerOK) {
+ log.warn("never expected here, add a request to preallocate queue failed");
+ }
+ }
+ }
-}
+ if (hasException) {
+ log.warn(this.getServiceName() + " service has exception. so return null");
+ return null;
+ }
-通过切面来进行数据源的设置
-@Aspect
-@Component
-@Order(-1)
-public class DataSourceAspect {
+ AllocateRequest result = this.requestTable.get(nextFilePath);
+ try {
+ // 这里就异步等着
+ if (result != null) {
+ boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);
+ if (!waitOK) {
+ log.warn("create mmap timeout " + result.getFilePath() + " " + result.getFileSize());
+ return null;
+ } else {
+ this.requestTable.remove(nextFilePath);
+ return result.getMappedFile();
+ }
+ } else {
+ log.error("find preallocate mmap failed, this never happen");
+ }
+ } catch (InterruptedException e) {
+ log.warn(this.getServiceName() + " service has exception. ", e);
+ }
- @Pointcut("execution(* com.nicksxs.springdemo.service..*.*(..))")
- public void pointCut() {
+ return null;
+ }
- }
+而真正去执行文件操作的就是 AllocateMappedFileService的 run 方法
public void run() {
+ log.info(this.getServiceName() + " service started");
+ while (!this.isStopped() && this.mmapOperation()) {
- @Before("pointCut()")
- public void before(JoinPoint point)
- {
- Object target = point.getTarget();
- System.out.println(target.toString());
- String method = point.getSignature().getName();
- System.out.println(method);
- Class<?>[] classz = target.getClass().getInterfaces();
- Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
- .getMethod().getParameterTypes();
+ }
+ log.info(this.getServiceName() + " service end");
+ }
+private boolean mmapOperation() {
+ boolean isSuccess = false;
+ AllocateRequest req = null;
try {
- Method m = classz[0].getMethod(method, parameterTypes);
- System.out.println("method"+ m.getName());
- if (m.isAnnotationPresent(DataSource.class)) {
- DataSource data = m.getAnnotation(DataSource.class);
- System.out.println("dataSource:"+data.value());
- DatabaseContextHolder.putDatabaseType(DatabaseType.getDatabaseType(data.value()));
+ // 从阻塞队列里获取请求
+ req = this.requestQueue.take();
+ AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath());
+ if (null == expectedRequest) {
+ log.warn("this mmap request expired, maybe cause timeout " + req.getFilePath() + " "
+ + req.getFileSize());
+ return true;
+ }
+ if (expectedRequest != req) {
+ log.warn("never expected here, maybe cause timeout " + req.getFilePath() + " "
+ + req.getFileSize() + ", req:" + req + ", expectedRequest:" + expectedRequest);
+ return true;
}
- } catch (Exception e) {
- e.printStackTrace();
+ if (req.getMappedFile() == null) {
+ long beginTime = System.currentTimeMillis();
+
+ MappedFile mappedFile;
+ if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
+ try {
+ // 通过 transientStorePool 创建
+ mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
+ mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
+ } catch (RuntimeException e) {
+ log.warn("Use default implementation.");
+ // 默认创建
+ mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
+ }
+ } else {
+ // 默认创建
+ mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
+ }
+
+ long eclipseTime = UtilAll.computeEclipseTimeMilliseconds(beginTime);
+ if (eclipseTime > 10) {
+ int queueSize = this.requestQueue.size();
+ log.warn("create mappedFile spent time(ms) " + eclipseTime + " queue size " + queueSize
+ + " " + req.getFilePath() + " " + req.getFileSize());
+ }
+
+ // pre write mappedFile
+ if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
+ .getMapedFileSizeCommitLog()
+ &&
+ this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
+ mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
+ this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
+ }
+
+ req.setMappedFile(mappedFile);
+ this.hasException = false;
+ isSuccess = true;
+ }
+ } catch (InterruptedException e) {
+ log.warn(this.getServiceName() + " interrupted, possibly by shutdown.");
+ this.hasException = true;
+ return false;
+ } catch (IOException e) {
+ log.warn(this.getServiceName() + " service has exception. ", e);
+ this.hasException = true;
+ if (null != req) {
+ requestQueue.offer(req);
+ try {
+ Thread.sleep(1);
+ } catch (InterruptedException ignored) {
+ }
+ }
+ } finally {
+ if (req != null && isSuccess)
+ // 通知前面等待的
+ req.getCountDownLatch().countDown();
}
- }
+ return true;
+ }
- @After("pointCut()")
- public void after() {
- DatabaseContextHolder.clearDatabaseType();
- }
-}
-通过接口判断是否带有注解跟是注解的值,DatabaseType 的配置不太好,不过先忽略了,然后在切点后进行清理
-这是我 master1 的数据,
-
master2 的数据
-
然后跑一下简单的 demo,
-@Override
-public void run(String...args) {
- LOGGER.info("run here");
- System.out.println(studentService.queryOne());
- System.out.println(studentService.queryAnother());
-}
-看一下运行结果
-
其实这个方法应用场景不止可以用来迁移数据库,还能实现精细化的读写数据源分离之类的,算是做个简单记录和分享。
]]>这次碰到一个比较奇怪的问题,应该统一发布脚本统一给应用启动参数传了个 -Dserver.port=xxxx,其实这个端口会作为 dubbo 的服务端口,并且应用也不提供 web 服务,但是在启动的时候会报embedded servlet container failed to start. port xxxx was already in use就觉得有点奇怪,仔细看了启动参数猜测可能是这个问题,有可能是依赖的二方三方包带了 spring-web 的包,然后基于 springboot 的 auto configuration 会把这个自己加载,就在本地复现了下这个问题,结果的确是这个问题。
比较老的 springboot 版本,可以使用
-SpringApplication app = new SpringApplication(XXXXXApplication.class);
-app.setWebEnvironment(false);
-app.run(args);
-新版本的 springboot (>= 2.0.0)可以在 properties 里配置
-spring.main.web-application-type=none
-或者
-SpringApplication app = new SpringApplication(XXXXXApplication.class);
-app.setWebApplicationType(WebApplicationType.NONE);
-这个枚举里还有其他两种配置
-public enum WebApplicationType {
+ 聊一下 RocketMQ 的消息存储之 MMAP
+ /2021/09/04/%E8%81%8A%E4%B8%80%E4%B8%8B-RocketMQ-%E7%9A%84%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/
+ 这是个很大的话题了,可能会分成两部分说,第一部分就是所谓的零拷贝 ( zero-copy ),这一块其实也不新鲜,我对零拷贝的概念主要来自这篇文章,个人感觉写得非常好,在 rocketmq 中,最大的一块存储就是消息存储,也就是 CommitLog ,当然还有 ConsumeQueue 和 IndexFile,以及其他一些文件,CommitLog 的存储是以一个 1G 大小的文件作为存储单位,写完了就再建一个,那么如何提高这 1G 文件的读写效率呢,就是 mmap,传统意义的读写文件,read,write 都需要由系统调用,来回地在用户态跟内核态进行拷贝切换,
+read(file, tmp_buf, len);
+write(socket, tmp_buf, len);
- /**
- * The application should not run as a web application and should not start an
- * embedded web server.
- */
- NONE,
- /**
- * The application should run as a servlet-based web application and should start an
- * embedded servlet web server.
- */
- SERVLET,
- /**
- * The application should run as a reactive web application and should start an
- * embedded reactive web server.
- */
- REACTIVE
+![vms95Z]()
+如上面的图显示的,要在用户态跟内核态进行切换,数据还需要在内核缓冲跟用户缓冲之间拷贝多次,
+
+
+- 第一步是调用 read,需要在用户态切换成内核态,DMA模块从磁盘中读取文件,并存储在内核缓冲区,相当于是第一次复制
+- 数据从内核缓冲区被拷贝到用户缓冲区,read 调用返回,伴随着内核态又切换成用户态,完成了第二次复制
+- 然后是write 写入,这里也会伴随着用户态跟内核态的切换,数据从用户缓冲区被复制到内核空间缓冲区,完成了第三次复制,这次有点不一样的是数据不是在内核缓冲区了,会复制到 socket buffer 中。
+- write 系统调用返回,又切换回了用户态,然后数据由 DMA 拷贝到协议引擎。
+
+
+如此就能看出其实默认的读写操作代价是非常大的,而在 rocketmq 等高性能中间件中都有使用的零拷贝技术,其中 rocketmq 使用的是 mmap
+mmap
mmap基于 OS 的 mmap 的内存映射技术,通过MMU 映射文件,将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作,使随机读写文件和读写内存相似的速度。
+
+mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。
+
+tmp_buf = mmap(file, len);
+write(socket, tmp_buf, len);
-}
-相当于是把none 的类型和包括 servlet 和 reactive 放进了枚举类进行控制。
-]]>其实最开始是举重项目,侯志慧是女子 49 公斤级的冠军,这场比赛是全场都看,其实看中国队的举重比赛跟跳水有点像,每一轮都需要到最后才能等到中国队,跳水其实每轮都有,举重会按照自己报的试举重量进行排名,重量大的会在后面举,抓举和挺举各三次试举机会,有时候会看着比较焦虑,一直等不来,怕一上来就没试举成功,而且中国队一般试举重量就是很大的,容易一次试举不成功就马上下一次,连着举其实压力会非常大,说实话真的是外行看热闹,每次都是多懂一点点,这次由于实在是比较无聊,所以看的会比较专心点,对于对应的规则知识点也会多了解一点,同时对于举重,没想到我们国家的这些运动员有这么强,最后八块金牌拿了七块,有一块拿到银牌也是有点因为教练的策略问题,这里其实也稍微知道一点,因为报上去的试举重量是谁小谁先举,并且我们国家都是实力非常强的,所以都会报大一些,并且如果这个项目有实力相近的选手,会比竞对多报一公斤,这样子如果前面竞争对手没举成功,我们把握就很大了,最坏的情况即使对手试举成功了,我们还有机会搏一把,比如谌利军这样的,只是说说感想,举重运动员真的是个比较单纯的群体,而且训练是非常痛苦枯燥的,非常容易受伤,像挺举就有点会压迫呼吸通道,看到好几个都是脸憋得通红,甚至直接因为压迫气道而没法完成后面的挺举,像之前 16 年的举重比赛,有个运动员没成功夺冠就非常愧疚地哭着说对不起祖国,没有获得冠军,这是怎么样的一种歉疚,怎么样的一种纯粹的感情呢,相对应地来说,我又要举男足,男篮的例子了,很多人在那嘲笑我这样对男足男篮愤愤不平的人,说可能我这样的人都没交个税(从缴纳个税的数量比例来算有可能),只是这里有两个打脸的事情,我足额缴纳个税,接近 20%的薪资都缴了个税,并且我买的所有东西都缴了增值税,如果让我这样缴纳了个税,缴纳了增值税的有个人的投票权,我一定会投票不让男足男篮使用我缴纳我的税金,用我们的缴纳的税,打出这么烂的表现,想乒乓球混双,拿个亚军都会被喷,那可是世界第二了,而且是就输了那么一场,足球篮球呢,我觉得是一方面成绩差,因为比赛真的有状态跟心态的影响,偶尔有一场失误非常正常,NBA 被黑八的有这么多强队,但是如果像男足男篮,成绩是越来越差,用范志毅的话来说就是脸都不要了,还有就是精气神,要在比赛中打出胜负欲,保持这种争胜心,才有机会再进步,前火箭队主教练鲁迪·汤姆贾诺维奇的话,“永远不要低估冠军的决心”,即使我现在打不过你,我会在下一次,下下次打败你,竞技体育永远要有这种精神,可以接受一时的失败,但是要保持永远争胜的心。
-第一块金牌是杨倩拿下的,中国队拿奥运会首金也是有政治任务的,而恰恰杨倩这个金牌也有点碰巧是对手最后一枪失误了,当然竞技体育,特别是射击,真的是容不得一点点失误,像前面几届的美国神通埃蒙斯,失之毫厘差之千里,但是这个具体评价就比较少,唯一一点让我比较出戏的就是杨倩真的非常像王刚的徒弟漆二娃,哈哈,微博上也有挺多人觉得像,射击还是个比较可以接受年纪稍大的运动员,需要经验和稳定性,相对来说爆发力体力稍好一点,像庞伟这样的,混合团体10米气手枪金牌,36 岁可能其他项目已经是年龄很大了,不过前面说的举重的吕小军军神也是年纪蛮大了,但是非常强,而且在油管上简直就是个神,相对来说射击是关注比较少,杨倩的也只是看了后面拿到冠军这个结果,有些因为时间或者电视上没放,但是成绩还是不错的,没多少喷点。
-第二篇先到这,纯主观,轻喷。
-]]>还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。
-这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。
-乒乓球的说的有点多,就分篇说了,第一篇先到这。
+
+]]>第一步:mmap系统调用使得文件内容被DMA引擎复制到内核缓冲区。然后该缓冲区与用户进程共享,在内核和用户内存空间之间不进行任何拷贝。
+第二步:写系统调用使得内核将数据从原来的内核缓冲区复制到与套接字相关的内核缓冲区。
+第三步:第三次拷贝发生在DMA引擎将数据从内核套接字缓冲区传递给协议引擎时。
+通过使用mmap而不是read,我们将内核需要拷贝的数据量减少了一半。当大量的数据被传输时,这将有很好的效果。然而,这种改进并不是没有代价的;在使用mmap+write方法时,有一些隐藏的陷阱。例如当你对一个文件进行内存映射,然后在另一个进程截断同一文件时调用写。你的写系统调用将被总线错误信号SIGBUS打断,因为你执行了一个错误的内存访问。该信号的默认行为是杀死进程并dumpcore–这对网络服务器来说不是最理想的操作。
+有两种方法可以解决这个问题。
+第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用返回。通过这样做,写系统调用会返回它在被打断之前所写的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,一个治标不治本的解决方案。因为SIGBUS预示着进程出了严重的问题,所以不鼓励使用这种解决方案。
+第二个解决方案涉及内核的文件租赁(在Windows中称为 “机会锁”)。这是解决这个问题的正确方法。通过在文件描述符上使用租赁,你与内核在一个特定的文件上达成租约。然后你可以向内核请求一个读/写租约。当另一个进程试图截断你正在传输的文件时,内核会向你发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉你内核即将终止你对该文件的写或读租约。在你的程序访问一个无效的地址和被SIGBUS信号杀死之前,你的写调用会被打断了。写入调用的返回值是中断前写入的字节数,errno将被设置为成功。下面是一些示例代码,显示了如何从内核中获得租约。
+if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { + perror("kernel lease set signal"); + return -1; +} +/* l_type can be F_RDLCK F_WRLCK */ +if(fcntl(fd, F_SETLEASE, l_type)){ + perror("kernel lease set type"); + return -1; +}
这个应该是 Dubbo SPI 里最玄妙的东西了,一开始没懂,自适应扩展点加载,dubbo://123.123.123.123:1234/com.nicksxs.demo.service.HelloWorldService?anyhost=true&application=demo&default.loadbalance=random&default.service.filter=LoggerFilter&dubbo=2.5.3&interface=com.nicksxs.demo.service.HelloWorldService&logger=slf4j&methods=method1,method2,method3,method4&pid=4292&retries=0&side=provider&threadpool=fixed&threads=200&timeout=2000×tamp=1590647155886
那我从比较能理解的角度或者说思路去讲讲我的理解,因为直接将原理如果脱离了使用,对于我这样的理解能力比较差的可能会比较吃力,从使用场景开始讲可能会比较舒服了,这里可以看到参数里有蛮多的,举个例子,比如这个 threadpool = fixed,说明线程池使用的是 fixed 对应的实现,也就是下图的这个
这样子似乎没啥问题了,反正就是用dubbo 的 spi 加载嘛,好像没啥问题,其实问题还是存在的,或者说不太优雅,比如要先判断我这个 fixed 对应的实现类是哪个,这里可能就有个 if-else 判断了,但是 dubbo 的开发人员似乎不太想这么做这个事情,
譬如我们在引用一个服务时,在ReferenceConfig 中的
-private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
+ IndexFile 的构建则是分发给这个进行处理
+class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
-就获取了自适应拓展,
-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);
+ @Override
+ public void dispatch(DispatchRequest request) {
+ if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
+ DefaultMessageStore.this.indexService.buildIndex(request);
+ }
+ }
+}
+public void buildIndex(DispatchRequest req) {
+ IndexFile indexFile = retryGetAndCreateIndexFile();
+ if (indexFile != null) {
+ long endPhyOffset = indexFile.getEndPhyOffset();
+ DispatchRequest msg = req;
+ String topic = msg.getTopic();
+ String keys = msg.getKeys();
+ if (msg.getCommitLogOffset() < endPhyOffset) {
+ return;
+ }
+
+ final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
+ switch (tranType) {
+ case MessageSysFlag.TRANSACTION_NOT_TYPE:
+ case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
+ case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
+ break;
+ case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
+ return;
+ }
+
+ if (req.getUniqKey() != null) {
+ indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
+ if (indexFile == null) {
+ log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
+ return;
+ }
+ }
+
+ if (keys != null && keys.length() > 0) {
+ String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
+ for (int i = 0; i < keyset.length; i++) {
+ String key = keyset[i];
+ if (key.length() > 0) {
+ indexFile = putKey(indexFile, msg, buildKey(topic, key));
+ if (indexFile == null) {
+ log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
+ return;
}
}
}
- } else {
- throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
+ } else {
+ log.error("build index error, stop building index");
}
+ }
- return (T) instance;
- }
-
-这里也使用了 DCL,来锁cachedAdaptiveInstance,当缓存中没有时就去创建自适应拓展
-private T createAdaptiveExtension() {
- try {
- // 获取自适应拓展类然后实例化
- return injectExtension((T) getAdaptiveExtensionClass().newInstance());
- } catch (Exception e) {
- throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
- }
- }
-
-private Class<?> getAdaptiveExtensionClass() {
- // 这里会获取拓展类,如果没有自适应的拓展类,那么就需要调用createAdaptiveExtensionClass
- getExtensionClasses();
- if (cachedAdaptiveClass != null) {
- return cachedAdaptiveClass;
- }
- return cachedAdaptiveClass = createAdaptiveExtensionClass();
- }
-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);
- }
+配置的数量
+private boolean messageIndexEnable = true;
+private int maxHashSlotNum = 5000000;
+private int maxIndexNum = 5000000 * 4;
-生成的代码像这样
-package com.alibaba.dubbo.rpc;
+最核心的其实是 IndexFile 的结构和如何写入
+public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
+ if (this.indexHeader.getIndexCount() < this.indexNum) {
+ // 获取 key 的 hash
+ int keyHash = indexKeyHashMethod(key);
+ // 计算属于哪个 slot
+ int slotPos = keyHash % this.hashSlotNum;
+ // 计算 slot 位置 因为结构是有个 indexHead,主要是分为三段 header,slot 和 index
+ int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
-import com.alibaba.dubbo.common.extension.ExtensionLoader;
+ FileLock fileLock = null;
+ try {
-public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
- public void destroy() {
- throw new UnsupportedOperationException(
- "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
- }
+ // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
+ // false);
+ int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
+ if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
+ slotValue = invalidIndex;
+ }
- public int getDefaultPort() {
- throw new UnsupportedOperationException(
- "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
- }
+ long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
- public com.alibaba.dubbo.rpc.Exporter export(
- com.alibaba.dubbo.rpc.Invoker arg0)
- throws com.alibaba.dubbo.rpc.RpcException {
- if (arg0 == null) {
- throw new IllegalArgumentException(
- "com.alibaba.dubbo.rpc.Invoker argument == null");
- }
+ timeDiff = timeDiff / 1000;
- if (arg0.getUrl() == null) {
- throw new IllegalArgumentException(
- "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
- }
+ if (this.indexHeader.getBeginTimestamp() <= 0) {
+ timeDiff = 0;
+ } else if (timeDiff > Integer.MAX_VALUE) {
+ timeDiff = Integer.MAX_VALUE;
+ } else if (timeDiff < 0) {
+ timeDiff = 0;
+ }
- com.alibaba.dubbo.common.URL url = arg0.getUrl();
- String extName = ((url.getProtocol() == null) ? "dubbo"
- : url.getProtocol());
+ // 计算索引存放位置,头部 + slot 数量 * slot 大小 + 已有的 index 数量 + index 大小
+ int absIndexPos =
+ IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
+ + this.indexHeader.getIndexCount() * indexSize;
+
+ this.mappedByteBuffer.putInt(absIndexPos, keyHash);
+ this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
+ this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
+ this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
- if (extName == null) {
- throw new IllegalStateException(
- "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
- url.toString() + ") use keys([protocol])");
- }
+ // 存放的是数量位移,不是绝对位置
+ this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
- com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
- .getExtension(extName);
+ if (this.indexHeader.getIndexCount() <= 1) {
+ this.indexHeader.setBeginPhyOffset(phyOffset);
+ this.indexHeader.setBeginTimestamp(storeTimestamp);
+ }
- return extension.export(arg0);
- }
+ this.indexHeader.incHashSlotCount();
+ this.indexHeader.incIndexCount();
+ this.indexHeader.setEndPhyOffset(phyOffset);
+ this.indexHeader.setEndTimestamp(storeTimestamp);
- public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,
- com.alibaba.dubbo.common.URL arg1)
- throws com.alibaba.dubbo.rpc.RpcException {
- if (arg1 == null) {
- throw new IllegalArgumentException("url == null");
+ return true;
+ } catch (Exception e) {
+ log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
+ } finally {
+ if (fileLock != null) {
+ try {
+ fileLock.release();
+ } catch (IOException e) {
+ log.error("Failed to release the lock", e);
+ }
+ }
+ }
+ } else {
+ log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
+ + "; index max num = " + this.indexNum);
}
- com.alibaba.dubbo.common.URL url = arg1;
- // 其实前面所说的逻辑就在这里呈现了
- String extName = ((url.getProtocol() == null) ? "dubbo"
- : url.getProtocol());
-
- if (extName == null) {
- throw new IllegalStateException(
- "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
- url.toString() + ") use keys([protocol])");
- }
- // 在这就是实际的通过dubbo 的 spi 去加载实际对应的扩展
- com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
- .getExtension(extName);
+ return false;
+ }
- return extension.refer(arg0, arg1);
- }
-}
-
+具体可以看一下这个简略的示意图
然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类
首先这里的场景跟我原来用的有点点区别,在项目中使用的是通过配置中心控制数据源切换,统一切换,而这里的例子多加了个可以根据接口注解配置
+第一部分是最核心的,如何基于 Spring JDBC 和 Druid 来实现数据源切换,是继承了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 这个类,他的determineCurrentLookupKey方法会被调用来获得用来决定选择那个数据源的对象,也就是 lookupKey,也可以通过这个类看到就是通过这个 lookupKey 来路由找到数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
+
+ @Override
+ protected Object determineCurrentLookupKey() {
+ if (DatabaseContextHolder.getDatabaseType() != null) {
+ return DatabaseContextHolder.getDatabaseType().getName();
+ }
+ return DatabaseType.MASTER1.getName();
+ }
+}
+
+而如何使用这个 lookupKey 呢,就涉及到我们的 DataSource 配置了,原来就是我们可以直接通过spring 的 jdbc 配置数据源,像这样
+
现在我们要使用 Druid 作为数据源了,然后配置 DynamicDataSource 的参数,通过 key 来选择对应的 DataSource,也就是下面配的 master1 和 master2
<bean id="master1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
+ destroy-method="close"
+ p:driverClassName="com.mysql.cj.jdbc.Driver"
+ p:url="${master1.demo.datasource.url}"
+ p:username="${master1.demo.datasource.username}"
+ p:password="${master1.demo.datasource.password}"
+ p:initialSize="5"
+ p:minIdle="1"
+ p:maxActive="10"
+ p:maxWait="60000"
+ p:timeBetweenEvictionRunsMillis="60000"
+ p:minEvictableIdleTimeMillis="300000"
+ p:validationQuery="SELECT 'x'"
+ p:testWhileIdle="true"
+ p:testOnBorrow="false"
+ p:testOnReturn="false"
+ p:poolPreparedStatements="false"
+ p:maxPoolPreparedStatementPerConnectionSize="20"
+ p:connectionProperties="config.decrypt=true"
+ p:filters="stat,config"/>
+
+ <bean id="master2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
+ destroy-method="close"
+ p:driverClassName="com.mysql.cj.jdbc.Driver"
+ p:url="${master2.demo.datasource.url}"
+ p:username="${master2.demo.datasource.username}"
+ p:password="${master2.demo.datasource.password}"
+ p:initialSize="5"
+ p:minIdle="1"
+ p:maxActive="10"
+ p:maxWait="60000"
+ p:timeBetweenEvictionRunsMillis="60000"
+ p:minEvictableIdleTimeMillis="300000"
+ p:validationQuery="SELECT 'x'"
+ p:testWhileIdle="true"
+ p:testOnBorrow="false"
+ p:testOnReturn="false"
+ p:poolPreparedStatements="false"
+ p:maxPoolPreparedStatementPerConnectionSize="20"
+ p:connectionProperties="config.decrypt=true"
+ p:filters="stat,config"/>
+
+ <bean id="dataSource" class="com.nicksxs.springdemo.config.DynamicDataSource">
+ <property name="targetDataSources">
+ <map key-type="java.lang.String">
+ <!-- master -->
+ <entry key="master1" value-ref="master1"/>
+ <!-- slave -->
+ <entry key="master2" value-ref="master2"/>
+ </map>
+ </property>
+ <property name="defaultTargetDataSource" ref="master1"/>
+ </bean>
+
+现在就要回到头上,介绍下这个DatabaseContextHolder,这里使用了 ThreadLocal 存放这个 DatabaseType,为啥要用这个是因为前面说的我们想要让接口层面去配置不同的数据源,要把持相互隔离不受影响,就使用了 ThreadLocal,关于它也可以看我前面写的一篇文章聊聊传说中的 ThreadLocal,而 DatabaseType 就是个简单的枚举
public class DatabaseContextHolder {
+ public static final ThreadLocal<DatabaseType> databaseTypeThreadLocal = new ThreadLocal<>();
+
+ public static DatabaseType getDatabaseType() {
+ return databaseTypeThreadLocal.get();
+ }
+
+ public static void putDatabaseType(DatabaseType databaseType) {
+ databaseTypeThreadLocal.set(databaseType);
+ }
+
+ public static void clearDatabaseType() {
+ databaseTypeThreadLocal.remove();
+ }
+}
+public enum DatabaseType {
+ MASTER1("master1", "1"),
+ MASTER2("master2", "2");
+
+ private final String name;
+ private final String value;
+
+ DatabaseType(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static DatabaseType getDatabaseType(String name) {
+ if (MASTER2.name.equals(name)) {
+ return MASTER2;
+ }
+ return MASTER1;
+ }
+}
+
+这边可以看到就是通过动态地通过putDatabaseType设置lookupKey来进行数据源切换,要通过接口注解配置来进行设置的话,我们就需要一个注解
@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface DataSource {
+ String value();
+}
+
+这个注解可以配置在我的接口方法上,比如这样
+public interface StudentService {
+
+ @DataSource("master1")
+ public Student queryOne();
+
+ @DataSource("master2")
+ public Student queryAnother();
+
+}
+
+通过切面来进行数据源的设置
+@Aspect
+@Component
+@Order(-1)
+public class DataSourceAspect {
+
+ @Pointcut("execution(* com.nicksxs.springdemo.service..*.*(..))")
+ public void pointCut() {
+
+ }
+
+
+ @Before("pointCut()")
+ public void before(JoinPoint point)
+ {
+ Object target = point.getTarget();
+ System.out.println(target.toString());
+ String method = point.getSignature().getName();
+ System.out.println(method);
+ Class<?>[] classz = target.getClass().getInterfaces();
+ Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
+ .getMethod().getParameterTypes();
+ try {
+ Method m = classz[0].getMethod(method, parameterTypes);
+ System.out.println("method"+ m.getName());
+ if (m.isAnnotationPresent(DataSource.class)) {
+ DataSource data = m.getAnnotation(DataSource.class);
+ System.out.println("dataSource:"+data.value());
+ DatabaseContextHolder.putDatabaseType(DatabaseType.getDatabaseType(data.value()));
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @After("pointCut()")
+ public void after() {
+ DatabaseContextHolder.clearDatabaseType();
+ }
+}
+
+通过接口判断是否带有注解跟是注解的值,DatabaseType 的配置不太好,不过先忽略了,然后在切点后进行清理
+这是我 master1 的数据,
+
master2 的数据
+
然后跑一下简单的 demo,
+@Override
+public void run(String...args) {
+ LOGGER.info("run here");
+ System.out.println(studentService.queryOne());
+ System.out.println(studentService.queryAnother());
+
+}
+
+看一下运行结果
+
其实这个方法应用场景不止可以用来迁移数据库,还能实现精细化的读写数据源分离之类的,算是做个简单记录和分享。
+]]>这次碰到一个比较奇怪的问题,应该统一发布脚本统一给应用启动参数传了个 -Dserver.port=xxxx,其实这个端口会作为 dubbo 的服务端口,并且应用也不提供 web 服务,但是在启动的时候会报embedded servlet container failed to start. port xxxx was already in use就觉得有点奇怪,仔细看了启动参数猜测可能是这个问题,有可能是依赖的二方三方包带了 spring-web 的包,然后基于 springboot 的 auto configuration 会把这个自己加载,就在本地复现了下这个问题,结果的确是这个问题。
比较老的 springboot 版本,可以使用
+SpringApplication app = new SpringApplication(XXXXXApplication.class);
+app.setWebEnvironment(false);
+app.run(args);
+新版本的 springboot (>= 2.0.0)可以在 properties 里配置
+spring.main.web-application-type=none
+或者
+SpringApplication app = new SpringApplication(XXXXXApplication.class);
+app.setWebApplicationType(WebApplicationType.NONE);
+这个枚举里还有其他两种配置
+public enum WebApplicationType {
+
+ /**
+ * The application should not run as a web application and should not start an
+ * embedded web server.
*/
- protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
- if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
- return bean;
- }
- if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
- return bean;
- }
- if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
- this.advisedBeans.put(cacheKey, Boolean.FALSE);
- return bean;
- }
+ NONE,
- // Create proxy if we have advice.
- Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
- if (specificInterceptors != DO_NOT_PROXY) {
- this.advisedBeans.put(cacheKey, Boolean.TRUE);
- Object proxy = createProxy(
- bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
- this.proxyTypes.put(cacheKey, proxy.getClass());
- return proxy;
- }
+ /**
+ * The application should run as a servlet-based web application and should start an
+ * embedded servlet web server.
+ */
+ SERVLET,
- this.advisedBeans.put(cacheKey, Boolean.FALSE);
- return bean;
- }
+ /**
+ * The application should run as a reactive web application and should start an
+ * embedded reactive web server.
+ */
+ REACTIVE
-然后在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy 中创建了代理类
相当于是把none 的类型和包括 servlet 和 reactive 放进了枚举类进行控制。
]]>其实最开始是举重项目,侯志慧是女子 49 公斤级的冠军,这场比赛是全场都看,其实看中国队的举重比赛跟跳水有点像,每一轮都需要到最后才能等到中国队,跳水其实每轮都有,举重会按照自己报的试举重量进行排名,重量大的会在后面举,抓举和挺举各三次试举机会,有时候会看着比较焦虑,一直等不来,怕一上来就没试举成功,而且中国队一般试举重量就是很大的,容易一次试举不成功就马上下一次,连着举其实压力会非常大,说实话真的是外行看热闹,每次都是多懂一点点,这次由于实在是比较无聊,所以看的会比较专心点,对于对应的规则知识点也会多了解一点,同时对于举重,没想到我们国家的这些运动员有这么强,最后八块金牌拿了七块,有一块拿到银牌也是有点因为教练的策略问题,这里其实也稍微知道一点,因为报上去的试举重量是谁小谁先举,并且我们国家都是实力非常强的,所以都会报大一些,并且如果这个项目有实力相近的选手,会比竞对多报一公斤,这样子如果前面竞争对手没举成功,我们把握就很大了,最坏的情况即使对手试举成功了,我们还有机会搏一把,比如谌利军这样的,只是说说感想,举重运动员真的是个比较单纯的群体,而且训练是非常痛苦枯燥的,非常容易受伤,像挺举就有点会压迫呼吸通道,看到好几个都是脸憋得通红,甚至直接因为压迫气道而没法完成后面的挺举,像之前 16 年的举重比赛,有个运动员没成功夺冠就非常愧疚地哭着说对不起祖国,没有获得冠军,这是怎么样的一种歉疚,怎么样的一种纯粹的感情呢,相对应地来说,我又要举男足,男篮的例子了,很多人在那嘲笑我这样对男足男篮愤愤不平的人,说可能我这样的人都没交个税(从缴纳个税的数量比例来算有可能),只是这里有两个打脸的事情,我足额缴纳个税,接近 20%的薪资都缴了个税,并且我买的所有东西都缴了增值税,如果让我这样缴纳了个税,缴纳了增值税的有个人的投票权,我一定会投票不让男足男篮使用我缴纳我的税金,用我们的缴纳的税,打出这么烂的表现,想乒乓球混双,拿个亚军都会被喷,那可是世界第二了,而且是就输了那么一场,足球篮球呢,我觉得是一方面成绩差,因为比赛真的有状态跟心态的影响,偶尔有一场失误非常正常,NBA 被黑八的有这么多强队,但是如果像男足男篮,成绩是越来越差,用范志毅的话来说就是脸都不要了,还有就是精气神,要在比赛中打出胜负欲,保持这种争胜心,才有机会再进步,前火箭队主教练鲁迪·汤姆贾诺维奇的话,“永远不要低估冠军的决心”,即使我现在打不过你,我会在下一次,下下次打败你,竞技体育永远要有这种精神,可以接受一时的失败,但是要保持永远争胜的心。
+第一块金牌是杨倩拿下的,中国队拿奥运会首金也是有政治任务的,而恰恰杨倩这个金牌也有点碰巧是对手最后一枪失误了,当然竞技体育,特别是射击,真的是容不得一点点失误,像前面几届的美国神通埃蒙斯,失之毫厘差之千里,但是这个具体评价就比较少,唯一一点让我比较出戏的就是杨倩真的非常像王刚的徒弟漆二娃,哈哈,微博上也有挺多人觉得像,射击还是个比较可以接受年纪稍大的运动员,需要经验和稳定性,相对来说爆发力体力稍好一点,像庞伟这样的,混合团体10米气手枪金牌,36 岁可能其他项目已经是年龄很大了,不过前面说的举重的吕小军军神也是年纪蛮大了,但是非常强,而且在油管上简直就是个神,相对来说射击是关注比较少,杨倩的也只是看了后面拿到冠军这个结果,有些因为时间或者电视上没放,但是成绩还是不错的,没多少喷点。
+第二篇先到这,纯主观,轻喷。
+]]>还有奥运会像乒乓球,篮球,跳水这几个都是比较喜欢的项目,篮球🏀是从初中开始就也有在自己在玩的,虽然因为身高啊体质基本没什么天赋,但也算是热爱驱动,差不多到了大学因为比较懒才放下了,初中高中还是有很多时间花在上面,不像别人经常打球跑跑跳跳还能长高,我反而一直都没长个子,也因为这个其实蛮遗憾的,后面想想可能是初中的时候远走他乡去住宿读初中,伙食营养跟不上导致的,可能也是自己的一厢情愿吧,总觉得应该还能再长点个,这一点以后我自己的小孩我应该会特别注意这段时间他/她的营养摄入了;然后像乒乓球🏓的话其实小时候是比较讨厌的,因为家里人,父母都没有这类爱好习惯,我也完全不会,但是小学那会班里的“恶霸”就以公平之名要我们男生每个人都排队打几个,我这种不会的反而又要被嘲笑,这个小时候的阴影让我有了比较不好的印象,对它🏓的改观是在工作以后,前司跟一个同样不会的同事经常在饭点会打打,而且那会因为这个其实身体得到了锻炼,感觉是个不错的健身方式,然后又是中国的优势项目,小时候跟着我爸看孔令辉,那时候完全不懂,印象就觉得老瓦很牛,后面其实也没那么关注,上一届好像看了马龙的比赛;跳水也是中国的优势项目,而且也比较简单,不是说真的很简单,就是我们外行观众看着就看看水花大小图一乐。
+这次的观赛过程其实主要还是在乒乓球上面,现在都有点怪我的乌鸦嘴,混双我一直就不太放心(关我什么事,我也不专业),然后一直觉得混双是不是不太稳,结果那天看的时候也是因为央视一套跟五套都没放,我家的有线电视又是没有五加体育,然后用电脑投屏就很卡,看得也很不爽,同时那天因为看的时候已经是 2:0还是再后面点了,一方面是不懂每队只有一次暂停,另一方面不知道已经用过暂停了,所以就特别怀疑马林是不是只会无脑鼓掌,感觉作为教练,并且是前冠军,应该也能在擦汗间隙,或者局间休息调整的时候多给些战略战术的指导,类似于后面男团小胖打奥恰洛夫,像解说都看出来了,其实奥恰那会的反手特别顺,打得特别凶,那就不能让他能特别顺手的上反手位,这当然是外行比较粗浅的看法,在混双过程中其实除了这个,还有让人很不爽的就是我们的许昕跟刘诗雯有种拿不出破釜沉舟的勇气的感觉,在气势上完全被对面两位日本乒乓球最讨厌的两位对手压制着,我都要输了,我就每一颗都要不让你好过,因为真的不是说没有实力,对面水谷隼也不是多么多么强的,可能上一届男团许昕输给他还留着阴影,但是以许昕 19 年男单世界第一的实力,目前也排在世界前三,输一场不应该成为这种阻力,有一些失误也很可惜,后面孙颖莎真的打得很解气,第二局一度以为又要被翻盘了,结果来了个大逆转,女团的时候也是,感觉在心态上孙颖莎还是很值得肯定的,少年老成这个词很适合,看其他的视频也觉得莎莎萌萌哒,陈梦总感觉还欠一点王者霸气,王曼昱还是可以的,反手很凶,我觉得其实这一届日本女乒就是打得非常凶,即使像平野这种看着很弱的妹子,打的球可一点都不弱,也是这种凶狠的打法,有点要压制中国的感觉,这方面我觉得是需要改善的,打这种要不就是实力上的完全碾压,要不就是我实力虽然比较没强多少,但是你狠我打得比你还狠,越保守越要输,我不太成熟的想法是这样的,还有就是面对逆境,这个就要说到男队的了,樊振东跟马龙在半决赛的时候,特别是男团的第二盘,樊振东打奥恰很好地表现了这个心态,当然樊振东我不是特别了解,据说他是比较善于打相持,比较善于焦灼的情况,不过整体看下来樊振东还是有一些欠缺,就是面对情况的快速转变应对,这一点也是马龙特别强的,虽然看起来马龙真的是年纪大了点,没有 16 年那会满头发胶,油光锃亮的大背头和满脸胶原蛋白的意气风发,大范围运动能力也弱了一点,但是经验和能力的全面性也让他最终能再次站上巅峰,还是非常佩服的,这里提一下张继科,虽然可能天赋上是张继科更强点,但是男乒一直都是有强者出现,能为国家队付出这么多并且一直坚持的可不是人人都可以,即使现在同台竞技马龙打不过张继科我还是更喜欢马龙。再来说说我们的对手,主要分三部分,德国男乒,里面有波尔(我刚听到的时候在想怎么又出来个叫波尔的,是不是像举重的石智勇一样,又来一个同名的,结果是同一个,已经四十岁了),这真是个让人敬佩的对手,实力强,经验丰富,虽然男单有点可惜,但是帮助男团获得银牌,真的是起到了定海神针的作用;奥恰洛夫,以前完全不认识,或者说看过也忘了,这次是真的有点意外,竟然有这么个马龙护法,其实他也坦言非常想赢一次马龙,并且在半决赛也非常接近赢得比赛,是个实力非常强的对手,就是男团半决赛输给张本智和有点可惜,有点被打蒙的感觉,佛朗西斯卡的话也是实力不错的选手,就是可能被奥恰跟波尔的光芒掩盖了,跟波尔在男团第一盘男双的比赛中打败日本那对男双也是非常给力的,说实话,最后打国乒的时候的确是国乒实力更胜一筹,但是即使德国赢了我也是充满尊敬,拼的就是硬实力,就像第二盘奥恰打樊振东,反手是真的很强,反过来看奥恰可能也不是很善于快速调整,樊振东打出来自己的节奏,主攻奥恰的中路,他好像没什么好办法解决。再来说我最讨厌的日本,嗯,小日本,张本智和、水谷隼、伊藤美诚,一一评价下(我是外行,绝对主观评价),张本智和,父母也是中国人,原来叫张智和,改日本籍后加了个本,被微博网友笑称日本尖叫鸡,男单输给了斯洛文尼亚选手,男团里是赢了两场,但是在我看来其实实力上可能比不上全力的奥恰,主要是特别能叫,会干扰对手,如果觉得这种也是种能力我也无话可说,要是有那种吼声能直接把对手震聋的,都不需要打比赛了,我简单记了下,赢一颗球,他要叫八声,用 LD 的话来说烦都烦死了,心态是在面对一些困境顺境的应对调整适应能力,而不是对这种噪音的适应能力,至少我是这么看的,所以我很期待樊振东能好好地虐虐他,因为其他像林昀儒真的是非常优秀的新选手,所谓的国乒克星估计也是小日本自己说说的,国乒其实有很多对手,马龙跟樊振东在男单半决赛碰到的这两个几乎都差点把他们掀翻了,所以还是练好自己的实力再来吹吧,免得打脸;水谷隼的话真的是长相就是特别地讨厌,还搞出那套不打比赛的姿态,男团里被波尔干掉就是很好的例子,波尔虽然真的很强,但毕竟 40 岁了,跟伊藤美诚一起说了吧,伊藤实力说实话是有的,混双中很大一部分的赢面来自于她,刘诗雯做了手术状态不好,许昕失误稍多,但是这种赢球了就感觉我赢了你一辈子一场没输的感觉,还有那种不知道怎么形容的笑,实力强的正常打比赛的我都佩服,像女团决赛里,平野跟石川佳纯的打法其实也很凶狠,但是都是正常的比赛,即使中国队两位实力不济输了也很正常,这种就真的需要像孙颖莎这样的小魔王无视各种魔法攻击,无视你各种花里胡哨的打法的人好好教训一下,混双输了以后了解了下她,感觉实力真的不错,是个大威胁,但是其实我们孙颖莎也是经历了九个月的继续成长,像张怡宁也评价了她,可能后面就没什么空间了,当然如果由张怡宁来打她就更适合了,净整这些有的没的,就打得你没脾气。
+乒乓球的说的有点多,就分篇说了,第一篇先到这。
+]]>快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
-失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
-失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
-并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。
-广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。
+快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
+失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
+失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
+并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2” 来设置最大并行数。
+广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。
+]]> +这个应该是 Dubbo SPI 里最玄妙的东西了,一开始没懂,自适应扩展点加载,dubbo://123.123.123.123:1234/com.nicksxs.demo.service.HelloWorldService?anyhost=true&application=demo&default.loadbalance=random&default.service.filter=LoggerFilter&dubbo=2.5.3&interface=com.nicksxs.demo.service.HelloWorldService&logger=slf4j&methods=method1,method2,method3,method4&pid=4292&retries=0&side=provider&threadpool=fixed&threads=200&timeout=2000×tamp=1590647155886
那我从比较能理解的角度或者说思路去讲讲我的理解,因为直接将原理如果脱离了使用,对于我这样的理解能力比较差的可能会比较吃力,从使用场景开始讲可能会比较舒服了,这里可以看到参数里有蛮多的,举个例子,比如这个 threadpool = fixed,说明线程池使用的是 fixed 对应的实现,也就是下图的这个
这样子似乎没啥问题了,反正就是用dubbo 的 spi 加载嘛,好像没啥问题,其实问题还是存在的,或者说不太优雅,比如要先判断我这个 fixed 对应的实现类是哪个,这里可能就有个 if-else 判断了,但是 dubbo 的开发人员似乎不太想这么做这个事情,
譬如我们在引用一个服务时,在ReferenceConfig 中的
+private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
+
+就获取了自适应拓展,
+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;
+ }
+
+这里也使用了 DCL,来锁cachedAdaptiveInstance,当缓存中没有时就去创建自适应拓展
+private T createAdaptiveExtension() {
+ try {
+ // 获取自适应拓展类然后实例化
+ return injectExtension((T) getAdaptiveExtensionClass().newInstance());
+ } catch (Exception e) {
+ throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
+ }
+ }
+
+private Class<?> getAdaptiveExtensionClass() {
+ // 这里会获取拓展类,如果没有自适应的拓展类,那么就需要调用createAdaptiveExtensionClass
+ getExtensionClasses();
+ if (cachedAdaptiveClass != null) {
+ return cachedAdaptiveClass;
+ }
+ return cachedAdaptiveClass = createAdaptiveExtensionClass();
+ }
+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);
+ }
+
+生成的代码像这样
+package com.alibaba.dubbo.rpc;
+
+import com.alibaba.dubbo.common.extension.ExtensionLoader;
+
+
+public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
+ public void destroy() {
+ throw new UnsupportedOperationException(
+ "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
+ }
+
+ public int getDefaultPort() {
+ throw new UnsupportedOperationException(
+ "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
+ }
+
+ public com.alibaba.dubbo.rpc.Exporter export(
+ com.alibaba.dubbo.rpc.Invoker arg0)
+ throws com.alibaba.dubbo.rpc.RpcException {
+ if (arg0 == null) {
+ throw new IllegalArgumentException(
+ "com.alibaba.dubbo.rpc.Invoker argument == null");
+ }
+
+ if (arg0.getUrl() == null) {
+ throw new IllegalArgumentException(
+ "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
+ }
+
+ com.alibaba.dubbo.common.URL url = arg0.getUrl();
+ String extName = ((url.getProtocol() == null) ? "dubbo"
+ : url.getProtocol());
+
+ if (extName == null) {
+ throw new IllegalStateException(
+ "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
+ url.toString() + ") use keys([protocol])");
+ }
+
+ com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
+ .getExtension(extName);
+
+ return extension.export(arg0);
+ }
+
+ public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,
+ com.alibaba.dubbo.common.URL arg1)
+ throws com.alibaba.dubbo.rpc.RpcException {
+ if (arg1 == null) {
+ throw new IllegalArgumentException("url == null");
+ }
+
+ com.alibaba.dubbo.common.URL url = arg1;
+ // 其实前面所说的逻辑就在这里呈现了
+ String extName = ((url.getProtocol() == null) ? "dubbo"
+ : url.getProtocol());
+
+ if (extName == null) {
+ throw new IllegalStateException(
+ "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
+ url.toString() + ") use keys([protocol])");
+ }
+ // 在这就是实际的通过dubbo 的 spi 去加载实际对应的扩展
+ com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
+ .getExtension(extName);
+
+ return extension.refer(arg0, arg1);
+ }
+}
+
]]>java.lang.Object#equals
+public boolean equals(Object obj) {
+ return (this == obj);
+ }
+对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了java.lang.String#equals
public boolean equals(Object anObject) {
+ if (this == anObject) {
+ return true;
+ }
+ if (anObject instanceof String) {
+ String anotherString = (String)anObject;
+ int n = value.length;
+ if (n == anotherString.value.length) {
+ char v1[] = value;
+ char v2[] = anotherString.value;
+ int i = 0;
+ while (n-- != 0) {
+ if (v1[i] != v2[i])
+ return false;
+ i++;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+然后呢就是为啥一些书或者 effective java 中写了 equals 跟 hashCode 要一起重写,这里涉及到当对象作为 HashMap 的 key 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 object 的hashCode 是 native 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.
以上验证、准备、解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
+最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。
+]]>-H进入线程模式
+-H :Threads-mode operation
+ Instructs top to display individual threads. Without this command-line option a summation of all threads in each process is shown. Later
+ this can be changed with the `H' interactive command.
+这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样
执行后是这样
第二个就是排序了
SORTING of task window
+
+ For compatibility, this top supports most of the former top sort keys. Since this is primarily a service to former top users, these commands
+ do not appear on any help screen.
+ command sorted-field supported
+ A start time (non-display) No
+ M %MEM Yes
+ N PID Yes
+ P %CPU Yes
+ T TIME+ Yes
+
+ Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
+ mand. That will help ensure that the actual sort environment matches your intent.
+
+ The following interactive commands will only be honored when the current sort field is visible. The sort field might not be visible because:
+ 1) there is insufficient Screen Width
+ 2) the `f' interactive command turned it Off
+
+ < :Move-Sort-Field-Left
+ Moves the sort column to the left unless the current sort field is the first field being displayed.
+
+ > :Move-Sort-Field-Right
+ Moves the sort column to the right unless the current sort field is the last field being displayed.
+查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<和>直接左右移动排序列
前面的信息其实上次就看过了,后面就可以发现有个死锁了,
上面比较长,把主要的截出来,就是这边的,这点就很强大了。
惯例还是看一下帮助信息
这个相对命令比较多,不过因为现在 dump 下来我们可能会用文件模式,然后将文件下载下来使用 mat 进行分析,所以可以使用jmap -dump:live,format=b,file=heap.bin <pid>
命令照着上面看的就是打印活着的对象,然后以二进制格式,文件名叫 heap.bin 然后最后就是进程 id,打印出来以后可以用 mat 打开
这样就可以很清晰的看到应用里的各种信息,jmap 直接在命令中还可以看很多信息,比如使用jmap -histo <pid>打印对象的实例数和对象占用的内存
jmap -finalizerinfo <pid> 打印正在等候回收的对象
对于一些应用内存已经占满了,jstack 和 jmap 可能会连不上的情况,可以使用-F参数强制打印线程或者 dump 文件,但是要注意这两者使用的用户必须与 java 进程启动用户一致,并且使用的 jdk 也要一致
<dependency>
+ <groupId>org.apache.shardingsphere</groupId>
+ <artifactId>shardingsphere-jdbc-core</artifactId>
+ <version>5.0.0-beta</version>
+</dependency>
+因为前面有聊过 Spring Boot 的自动加载,在这里 spring 就会自己去找 DataSource 的配置,所以要在入口把它干掉
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
+public class ShardingJdbcDemoApplication implements CommandLineRunner {
+然后因为想在入口跑代码,就实现了下 org.springframework.boot.CommandLineRunner 主要是后面的 Java Config 代码
+// 注意这里的注解,可以让 Spring 自动帮忙加载,也就是 Java Config 的核心
+@Configuration
+public class MysqlConfig {
+
+ @Bean
+ public DataSource dataSource() throws SQLException {
+ // Configure actual data sources
+ Map<String, DataSource> dataSourceMap = new HashMap<>();
+
+
+ // Configure the first data source
+ // 使用了默认的Hikari连接池的 DataSource
+ HikariDataSource dataSource1 = new HikariDataSource();
+ dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
+ dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/sharding");
+ dataSource1.setUsername("username");
+ dataSource1.setPassword("password");
+ dataSourceMap.put("ds0", dataSource1);
+
+ // Configure student table rule
+ // 这里是配置分表逻辑,逻辑表是 student,对应真实的表是 student_0 到 student_1, 这个配置方式就是有多少表可以用 student_$->{0..n}
+ ShardingTableRuleConfiguration studentTableRuleConfig = new ShardingTableRuleConfiguration("student", "ds0.student_$->{0..1}");
+
+ // 设置分表字段
+ studentTableRuleConfig.setTableShardingStrategy(new StandardShardingStrategyConfiguration("user_id", "tableShardingAlgorithm"));
+
+
+ // Configure sharding rule
+ // 配置 studentTableRuleConfig
+ ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
+ shardingRuleConfig.getTables().add(studentTableRuleConfig);
+
+ // Configure table sharding algorithm
+ Properties tableShardingAlgorithmrProps = new Properties();
+ // 算法表达式就是根据 user_id 对 2 进行取模
+ tableShardingAlgorithmrProps.setProperty("algorithm-expression", "student_${user_id % 2}");
+ shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps));
+
+
+ // 然后创建这个 DataSource
+ return ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), new Properties());
-Java stack information for the threads listed above:
-===================================================
-"mythread2":
- at sun.misc.Unsafe.park(Native Method)
- - parking to wait for <0x000000076f5d4330> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
- at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
- at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
- at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
- at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$2.run(ThreadDumpDemoApplication.java:34)
-"mythread1":
- at sun.misc.Unsafe.park(Native Method)
- - parking to wait for <0x000000076f5d4360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
- at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
- at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
- at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
- at com.nicksxs.thread_dump_demo.ThreadDumpDemoApplication$1.run(ThreadDumpDemoApplication.java:22)
+ }
+}
+然后我们就可以在使用这个 DataSource 了,先看下这两个表的数据

@Override
+ public void run(String... args) {
+ LOGGER.info("run here");
+ String sql = "SELECT * FROM student WHERE user_id=? ";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ // 参数就是 user_id,然后也是分表键,对 2 取模就是 1,应该是去 student_1 取数据
+ ps.setInt(1, 1001);
-Found 1 deadlock.
-前面的信息其实上次就看过了,后面就可以发现有个死锁了,
上面比较长,把主要的截出来,就是这边的,这点就很强大了。
惯例还是看一下帮助信息
这个相对命令比较多,不过因为现在 dump 下来我们可能会用文件模式,然后将文件下载下来使用 mat 进行分析,所以可以使用jmap -dump:live,format=b,file=heap.bin <pid>
命令照着上面看的就是打印活着的对象,然后以二进制格式,文件名叫 heap.bin 然后最后就是进程 id,打印出来以后可以用 mat 打开
这样就可以很清晰的看到应用里的各种信息,jmap 直接在命令中还可以看很多信息,比如使用jmap -histo <pid>打印对象的实例数和对象占用的内存
jmap -finalizerinfo <pid> 打印正在等候回收的对象
对于一些应用内存已经占满了,jstack 和 jmap 可能会连不上的情况,可以使用-F参数强制打印线程或者 dump 文件,但是要注意这两者使用的用户必须与 java 进程启动用户一致,并且使用的 jdk 也要一致
看下查询结果
以上验证、准备、解析 三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
-最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。
+ResultSet resultSet = ps.executeQuery(sql);
+实际上应该是
+ResultSet resultSet = ps.executeQuery();
+而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系
这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里
@Override
+public final ResultSet executeQuery(final String sql) throws SQLException {
+ throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
+}
+这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础
看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,
在前一篇的 demo 中的
Connection conn = dataSource.getConnection();
+其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement
PreparedStatement ps = conn.prepareStatement(sql)
+就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行
ResultSet resultSet = ps.executeQuery();
+然后获得结果org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet
其实像 mybatis 也是基于这样去实现的
]]>java.lang.Object#equals
-public boolean equals(Object obj) {
- return (this == obj);
- }
-对于所有对象的父类,equals 方法其实对比的就是对象的地址,也就是是否是同一个对象,试想如果像 Integer 或者 String 这种,我们没有重写 equals,那其实就等于是在用==,可能就没法达到我们的目的,所以像 String 这种常用的 jdk 类都是默认重写了java.lang.String#equals
public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String)anObject;
- int n = value.length;
- if (n == anotherString.value.length) {
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = 0;
- while (n-- != 0) {
- if (v1[i] != v2[i])
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
-然后呢就是为啥一些书或者 effective java 中写了 equals 跟 hashCode 要一起重写,这里涉及到当对象作为 HashMap 的 key 的时候
首先 HashMap 会使用 hashCode 去判断是否在同一个槽里,然后在通过 equals 去判断是否是同一个 key,是的话就替换,不是的话就链表接下去,如果不重写 hashCode 的话,默认的 object 的hashCode 是 native 方法,根据对象的地址生成的,这样其实对象的值相同的话,因为地址不同,HashMap 也会出现异常,所以需要重写,同时也需要重写 equals 方法,才能确认是同一个 key,而不是落在同一个槽的不同 key.
-H进入线程模式
--H :Threads-mode operation
- Instructs top to display individual threads. Without this command-line option a summation of all threads in each process is shown. Later
- this can be changed with the `H' interactive command.
-这样就能用在 Java 中去 jstack 中找到对应的线程
其实还有比较重要的两个操作,
一个是在 top 启动状态下,按c键,这样能把比如说是一个 Java 进程,具体的进程命令显示出来
像这样
执行前是这样
执行后是这样
第二个就是排序了
SORTING of task window
+ 聊聊 Sharding-Jdbc 分库分表下的分页方案
+ /2022/01/09/%E8%81%8A%E8%81%8A-Sharding-Jdbc-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E4%B8%8B%E7%9A%84%E5%88%86%E9%A1%B5%E6%96%B9%E6%A1%88/
+ 前面在聊 Sharding-Jdbc 的时候看到了一篇文章,关于一个分页的查询,一直比较直接的想法就是分库分表下的分页是非常不合理的,一般我们的实操方案都是分表加上 ES 搜索做分页,或者通过合表读写分离的方案,因为对于 sharding-jdbc 如果没有带分表键,查询基本都是只能在所有分表都执行一遍,然后再加上分页,基本上是分页越大后续的查询越耗资源,但是仔细的去想这个细节还是这次,就简单说说
首先就是我的分表结构
+CREATE TABLE `student_time_0` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` int(11) NOT NULL,
+ `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
+ `age` tinyint(3) unsigned DEFAULT NULL,
+ `create_time` bigint(20) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=674 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
+有这样的三个表,student_time_0, student_time_1, student_time_2, 以 user_id 作为分表键,根据表数量取模作为分表依据
这里先构造点数据,
+insert into student_time (`name`, `user_id`, `age`, `create_time`) values (?, ?, ?, ?)
+主要是为了保证 create_time 唯一比较好说明问题,
+int i = 0;
+try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(insertSql)) {
+ do {
+ ps.setString(1, localName + new Random().nextInt(100));
+ ps.setLong(2, 10086L + (new Random().nextInt(100)));
+ ps.setInt(3, 18);
+ ps.setLong(4, new Date().getTime());
- For compatibility, this top supports most of the former top sort keys. Since this is primarily a service to former top users, these commands
- do not appear on any help screen.
- command sorted-field supported
- A start time (non-display) No
- M %MEM Yes
- N PID Yes
- P %CPU Yes
- T TIME+ Yes
- Before using any of the following sort provisions, top suggests that you temporarily turn on column highlighting using the `x' interactive com‐
- mand. That will help ensure that the actual sort environment matches your intent.
+ int result = ps.executeUpdate();
+ LOGGER.info("current execute result: {}", result);
+ Thread.sleep(new Random().nextInt(100));
+ i++;
+ } while (i <= 2000);
+三个表的数据分别是 673,678,650,说明符合预期了,各个表数据不一样,接下来比如我们想要做一个这样的分页查询
+select * from student_time ORDER BY create_time ASC limit 1000, 5;
+student_time 对于我们使用的 sharding-jdbc 来说当然是逻辑表,首先从一无所知去想这个查询如果我们自己来处理应该是怎么做,
首先是不是可以每个表都从 333 开始取 5 条数据,类似于下面的查询,然后进行 15 条的合并重排序获取前面的 5 条
+select * from student_time_0 ORDER BY create_time ASC limit 333, 5;
+select * from student_time_1 ORDER BY create_time ASC limit 333, 5;
+select * from student_time_2 ORDER BY create_time ASC limit 333, 5;
+忽略前面 limit 差的 1,这个结果除非三个表的分布是绝对的均匀,否则结果肯定会出现一定的偏差,以为每个表的 333 这个位置对于其他表来说都不一定是一样的,这样对于最后整体的结果,就会出现偏差
因为一直在纠结怎么让这个更直观的表现出来,所以尝试画了个图
![]()
黑色的框代表我从每个表里按排序从 334 到 338 的 5 条数据, 他们在每个表里都是代表了各自正确的排序值,但是对于我们想要的其实是合表后的 1001,1005 这五条,然后我们假设总的排序值位于前 1000 的分布是第 0 个表是 320 条,第 1 个表是 340 条,第 2 个表是 340 条,那么可以明显地看出来我这么查的结果简单合并肯定是不对的。
那么 sharding-jdbc 是如何保证这个结果的呢,其实就是我在每个表里都查分页偏移量和分页大小那么多的数据,在我这个例子里就是对于 0,1,2 三个分表每个都查 1005 条数据,即使我的数据不平衡到最极端的情况,前 1005 条数据都出在某个分表中,也可以正确获得最后的结果,但是明显的问题就是大分页,数据较多,就会导致非常大的问题,即使如 sharding-jdbc 对于合并排序的优化做得比较好,也还是需要传输那么大量的数据,并且查询也耗时,那么有没有解决方案呢,应该说有两个,或者说主要是想讲后者
第一个办法是像这种查询,如果业务上不需要进行跳页,而是只给下一页,那么我们就能把前一次的最大偏移量的 create_time 记录下来,下一页就可以拿着这个偏移量进行查询,这个比较简单易懂,就不多说了
第二个办法是看的58 沈剑的一篇文章,尝试理解讲述一下,
这个办法的第一步跟前面那个错误的方法或者说不准确的方法一样,先是将分页偏移量平均后在三个表里进行查询
+t0
+334 10158 nick95 18 1641548941767
+335 10098 nick11 18 1641548941879
+336 10167 nick51 18 1641548942089
+337 10167 nick3 18 1641548942119
+338 10170 nick57 18 1641548942169
- The following interactive commands will only be honored when the current sort field is visible. The sort field might not be visible because:
- 1) there is insufficient Screen Width
- 2) the `f' interactive command turned it Off
- < :Move-Sort-Field-Left
- Moves the sort column to the left unless the current sort field is the first field being displayed.
+t1
+334 10105 nick98 18 1641548939071 最小
+335 10174 nick94 18 1641548939377
+336 10129 nick85 18 1641548939442
+337 10141 nick84 18 1641548939480
+338 10096 nick74 18 1641548939668
+
+t2
+334 10184 nick11 18 1641548945075
+335 10109 nick93 18 1641548945382
+336 10181 nick41 18 1641548945583
+337 10130 nick80 18 1641548945993
+338 10184 nick19 18 1641548946294 最大
+然后要做什么呢,其实目标比较明白,因为前面那种方法其实就是我知道了前一页的偏移量,所以可以直接当做条件来进行查询,那这里我也想着拿到这个条件,所以我将第一遍查出来的最小的 create_time 和最大的 create_time 找出来,然后再去三个表里查询,其实主要是最小值,因为我拿着最小值去查以后我就能知道这个最小值在每个表里处在什么位置,
+t0
+322 10161 nick81 18 1641548939284
+323 10113 nick16 18 1641548939393
+324 10110 nick56 18 1641548939577
+325 10116 nick69 18 1641548939588
+326 10173 nick51 18 1641548939646
+
+t1
+334 10105 nick98 18 1641548939071
+335 10174 nick94 18 1641548939377
+336 10129 nick85 18 1641548939442
+337 10141 nick84 18 1641548939480
+338 10096 nick74 18 1641548939668
- > :Move-Sort-Field-Right
- Moves the sort column to the right unless the current sort field is the last field being displayed.
-查看 man page 可以找到这一段,其实一般 man page 都是最细致的,只不过因为太多了,有时候懒得看,这里可以通过大写 M 和大写 P 分别按内存和 CPU 排序,下面还有两个小技巧,通过按 x 可以将当前活跃的排序列用不同颜色标出来,然后可以通过<和>直接左右移动排序列
+t2
+297 10136 nick28 18 1641548939161
+298 10142 nick68 18 1641548939177
+299 10124 nick41 18 1641548939237
+300 10148 nick87 18 1641548939510
+301 10169 nick23 18 1641548939715
+我只贴了前五条数据,为了方便知道偏移量,每个分表都使用了自增主键,我们可以看到前一次查询的最小值分别在其他两个表里的位置分别是 322-1 和 297-1,那么对于总体来说这个时间应该是在 322 - 1 + 333 + 297 - 1 = 951,那这样子我只要对后面的数据最多每个表查 1000 - 951 + 5 = 54 条数据再进行合并排序就可以获得最终正确的结果。
这个就是传说中的二次查询法。
<dependency>
- <groupId>org.apache.shardingsphere</groupId>
- <artifactId>shardingsphere-jdbc-core</artifactId>
- <version>5.0.0-beta</version>
-</dependency>
-因为前面有聊过 Spring Boot 的自动加载,在这里 spring 就会自己去找 DataSource 的配置,所以要在入口把它干掉
-@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
-public class ShardingJdbcDemoApplication implements CommandLineRunner {
-然后因为想在入口跑代码,就实现了下 org.springframework.boot.CommandLineRunner 主要是后面的 Java Config 代码
-// 注意这里的注解,可以让 Spring 自动帮忙加载,也就是 Java Config 的核心
-@Configuration
-public class MysqlConfig {
-
- @Bean
- public DataSource dataSource() throws SQLException {
- // Configure actual data sources
- Map<String, DataSource> dataSourceMap = new HashMap<>();
-
-
- // Configure the first data source
- // 使用了默认的Hikari连接池的 DataSource
- HikariDataSource dataSource1 = new HikariDataSource();
- dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
- dataSource1.setJdbcUrl("jdbc:mysql://localhost:3306/sharding");
- dataSource1.setUsername("username");
- dataSource1.setPassword("password");
- dataSourceMap.put("ds0", dataSource1);
-
- // Configure student table rule
- // 这里是配置分表逻辑,逻辑表是 student,对应真实的表是 student_0 到 student_1, 这个配置方式就是有多少表可以用 student_$->{0..n}
- ShardingTableRuleConfiguration studentTableRuleConfig = new ShardingTableRuleConfiguration("student", "ds0.student_$->{0..1}");
-
- // 设置分表字段
- studentTableRuleConfig.setTableShardingStrategy(new StandardShardingStrategyConfiguration("user_id", "tableShardingAlgorithm"));
-
-
- // Configure sharding rule
- // 配置 studentTableRuleConfig
- ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
- shardingRuleConfig.getTables().add(studentTableRuleConfig);
-
- // Configure table sharding algorithm
- Properties tableShardingAlgorithmrProps = new Properties();
- // 算法表达式就是根据 user_id 对 2 进行取模
- tableShardingAlgorithmrProps.setProperty("algorithm-expression", "student_${user_id % 2}");
- shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps));
-
-
- // 然后创建这个 DataSource
- return ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), new Properties());
-
- }
-}
-然后我们就可以在使用这个 DataSource 了,先看下这两个表的数据

@Override
- public void run(String... args) {
- LOGGER.info("run here");
- String sql = "SELECT * FROM student WHERE user_id=? ";
- try (
- Connection conn = dataSource.getConnection();
- PreparedStatement ps = conn.prepareStatement(sql)) {
- // 参数就是 user_id,然后也是分表键,对 2 取模就是 1,应该是去 student_1 取数据
- ps.setInt(1, 1001);
-
- ResultSet resultSet = ps.executeQuery();
- while (resultSet.next()) {
- final int id = resultSet.getInt("id");
- final String name = resultSet.getString("name");
- final int userId = resultSet.getInt("user_id");
- final int age = resultSet.getInt("age");
- System.out.println("奇数表 id:" + id + " 姓名:" + name
- + " 用户 id:" + userId + " 年龄:" + age );
- System.out.println("=============================");
- }
- // 参数就是 user_id,然后也是分表键,对 2 取模就是 0,应该是去 student_0 取数据
- ps.setInt(1, 1000);
- resultSet = ps.executeQuery();
- while (resultSet.next()) {
- final int id = resultSet.getInt("id");
- final String name = resultSet.getString("name");
- final int userId = resultSet.getInt("user_id");
- final int age = resultSet.getInt("age");
- System.out.println("偶数表 id:" + id + " 姓名:" + name
- + " 用户 id:" + userId + " 年龄:" + age );
- System.out.println("=============================");
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
-看下查询结果
CREATE TABLE `student_time_0` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `user_id` int(11) NOT NULL,
- `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
- `age` tinyint(3) unsigned DEFAULT NULL,
- `create_time` bigint(20) DEFAULT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=674 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
-有这样的三个表,student_time_0, student_time_1, student_time_2, 以 user_id 作为分表键,根据表数量取模作为分表依据
这里先构造点数据,
insert into student_time (`name`, `user_id`, `age`, `create_time`) values (?, ?, ?, ?)
-主要是为了保证 create_time 唯一比较好说明问题,
int i = 0;
-try (
- Connection conn = dataSource.getConnection();
- PreparedStatement ps = conn.prepareStatement(insertSql)) {
- do {
- ps.setString(1, localName + new Random().nextInt(100));
- ps.setLong(2, 10086L + (new Random().nextInt(100)));
- ps.setInt(3, 18);
- ps.setLong(4, new Date().getTime());
-
-
- int result = ps.executeUpdate();
- LOGGER.info("current execute result: {}", result);
- Thread.sleep(new Random().nextInt(100));
- i++;
- } while (i <= 2000);
-三个表的数据分别是 673,678,650,说明符合预期了,各个表数据不一样,接下来比如我们想要做一个这样的分页查询
-select * from student_time ORDER BY create_time ASC limit 1000, 5;
-student_time 对于我们使用的 sharding-jdbc 来说当然是逻辑表,首先从一无所知去想这个查询如果我们自己来处理应该是怎么做,
首先是不是可以每个表都从 333 开始取 5 条数据,类似于下面的查询,然后进行 15 条的合并重排序获取前面的 5 条
select * from student_time_0 ORDER BY create_time ASC limit 333, 5;
-select * from student_time_1 ORDER BY create_time ASC limit 333, 5;
-select * from student_time_2 ORDER BY create_time ASC limit 333, 5;
-忽略前面 limit 差的 1,这个结果除非三个表的分布是绝对的均匀,否则结果肯定会出现一定的偏差,以为每个表的 333 这个位置对于其他表来说都不一定是一样的,这样对于最后整体的结果,就会出现偏差
因为一直在纠结怎么让这个更直观的表现出来,所以尝试画了个图
黑色的框代表我从每个表里按排序从 334 到 338 的 5 条数据, 他们在每个表里都是代表了各自正确的排序值,但是对于我们想要的其实是合表后的 1001,1005 这五条,然后我们假设总的排序值位于前 1000 的分布是第 0 个表是 320 条,第 1 个表是 340 条,第 2 个表是 340 条,那么可以明显地看出来我这么查的结果简单合并肯定是不对的。
那么 sharding-jdbc 是如何保证这个结果的呢,其实就是我在每个表里都查分页偏移量和分页大小那么多的数据,在我这个例子里就是对于 0,1,2 三个分表每个都查 1005 条数据,即使我的数据不平衡到最极端的情况,前 1005 条数据都出在某个分表中,也可以正确获得最后的结果,但是明显的问题就是大分页,数据较多,就会导致非常大的问题,即使如 sharding-jdbc 对于合并排序的优化做得比较好,也还是需要传输那么大量的数据,并且查询也耗时,那么有没有解决方案呢,应该说有两个,或者说主要是想讲后者
第一个办法是像这种查询,如果业务上不需要进行跳页,而是只给下一页,那么我们就能把前一次的最大偏移量的 create_time 记录下来,下一页就可以拿着这个偏移量进行查询,这个比较简单易懂,就不多说了
第二个办法是看的58 沈剑的一篇文章,尝试理解讲述一下,
这个办法的第一步跟前面那个错误的方法或者说不准确的方法一样,先是将分页偏移量平均后在三个表里进行查询
t0
-334 10158 nick95 18 1641548941767
-335 10098 nick11 18 1641548941879
-336 10167 nick51 18 1641548942089
-337 10167 nick3 18 1641548942119
-338 10170 nick57 18 1641548942169
-
-
-t1
-334 10105 nick98 18 1641548939071 最小
-335 10174 nick94 18 1641548939377
-336 10129 nick85 18 1641548939442
-337 10141 nick84 18 1641548939480
-338 10096 nick74 18 1641548939668
-
-t2
-334 10184 nick11 18 1641548945075
-335 10109 nick93 18 1641548945382
-336 10181 nick41 18 1641548945583
-337 10130 nick80 18 1641548945993
-338 10184 nick19 18 1641548946294 最大
-然后要做什么呢,其实目标比较明白,因为前面那种方法其实就是我知道了前一页的偏移量,所以可以直接当做条件来进行查询,那这里我也想着拿到这个条件,所以我将第一遍查出来的最小的 create_time 和最大的 create_time 找出来,然后再去三个表里查询,其实主要是最小值,因为我拿着最小值去查以后我就能知道这个最小值在每个表里处在什么位置,
-t0
-322 10161 nick81 18 1641548939284
-323 10113 nick16 18 1641548939393
-324 10110 nick56 18 1641548939577
-325 10116 nick69 18 1641548939588
-326 10173 nick51 18 1641548939646
-
-t1
-334 10105 nick98 18 1641548939071
-335 10174 nick94 18 1641548939377
-336 10129 nick85 18 1641548939442
-337 10141 nick84 18 1641548939480
-338 10096 nick74 18 1641548939668
-
-t2
-297 10136 nick28 18 1641548939161
-298 10142 nick68 18 1641548939177
-299 10124 nick41 18 1641548939237
-300 10148 nick87 18 1641548939510
-301 10169 nick23 18 1641548939715
-我只贴了前五条数据,为了方便知道偏移量,每个分表都使用了自增主键,我们可以看到前一次查询的最小值分别在其他两个表里的位置分别是 322-1 和 297-1,那么对于总体来说这个时间应该是在 322 - 1 + 333 + 297 - 1 = 951,那这样子我只要对后面的数据最多每个表查 1000 - 951 + 5 = 54 条数据再进行合并排序就可以获得最终正确的结果。
这个就是传说中的二次查询法。
ResultSet resultSet = ps.executeQuery(sql);
-实际上应该是
-ResultSet resultSet = ps.executeQuery();
-而这里的差别就是,是否传 sql 这个参数,首先我们要知道这个 ps 是什么,它也是个接口java.sql.PreparedStatement,而真正的实现类是org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement,我们来看下继承关系
这里可以看到继承关系里有org.apache.shardingsphere.driver.jdbc.unsupported.AbstractUnsupportedOperationPreparedStatement
那么在我上面的写错的代码里
@Override
-public final ResultSet executeQuery(final String sql) throws SQLException {
- throw new SQLFeatureNotSupportedException("executeQuery with SQL for PreparedStatement");
-}
-这个报错一开始让我有点懵,后来点进去了发现是这么个异常,但是我其实一开始是用的更新语句,以为更新不支持,因为平时使用没有深究过,以为是不是需要使用 Mybatis 才可以执行更新,但是理论上也不应该,再往上看原来这些异常是由 sharding-jdbc 包装的,也就是在上面说的AbstractUnsupportedOperationPreparedStatement,这其实也是一种设计思想,本身 jdbc 提供了一系列接口,由各家去支持,包括 mysql,sql server,oracle 等,而正因为这个设计,所以 sharding-jdbc 也可以在此基础上进行设计,我们可以总体地看下 sharding-jdbc 的实现基础
看了前面ShardingSpherePreparedStatement的继承关系,应该也能猜到这里的几个类都是实现了 jdbc 的基础接口,
在前一篇的 demo 中的
Connection conn = dataSource.getConnection();
-其实就获得了org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection#ShardingSphereConnection
然后获得java.sql.PreparedStatement
PreparedStatement ps = conn.prepareStatement(sql)
-就是获取了org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement
然后就是执行
ResultSet resultSet = ps.executeQuery();
-然后获得结果org.apache.shardingsphere.driver.jdbc.core.resultset.ShardingSphereResultSet
其实像 mybatis 也是基于这样去实现的
+缓存穿透是指当数据库中本身就不存在这个数据的时候,使用一般的缓存策略时访问不到缓存后就访问数据库,但是因为数据库也没数据,所以如果不做任何策略优化的话,这类数据就每次都会访问一次数据库,对数据库压力也会比较大。
+缓存击穿跟穿透比较类似的,都是访问缓存不在,然后去访问数据库,与穿透不一样的是击穿是在数据库中存在数据,但是可能由于第一次访问,或者缓存过期了,需要访问到数据库,这对于访问量小的情况其实算是个正常情况,但是随着请求量变高就会引发一些性能隐患。
+缓存雪崩就是击穿的大规模集群效应,当大量的缓存过期失效的时候,这些请求都是直接访问到数据库了,会对数据库造成很大的压力。
+对于以上三种场景也有一些比较常见的解决方案,但也不能说是万无一失的,需要随着业务去寻找合适的方案
+对于数据库中就没这个数据的时候,一种是可以对这个 key 设置下空值,即以一个特定的表示是数据库不存在的,这种情况需要合理地调整过期时间,当这个 key 在数据库中有数据了的话,也需要有策略去更新这个值,并且如果这类 key 非常多,这个方法就会不太合适,就可以使用第二种方法,就是布隆过滤器,bloom filter,前置一个布隆过滤器,当这个 key 在数据库不存在的话,先用布隆过滤器挡一道,如果不在的话就直接返回了,当然布隆过滤器不是绝对的准确的
+当一个 key 的缓存过期了,如果大量请求过来访问这个 key,请求都会落在数据库里,这个时候就可以使用一些类似于互斥锁的方式去让一个线程去访问数据库,更新缓存,但是这里其实也有个问题,就是如果是热点 key 其实这种方式也比较危险,万一更新失败,或者更新操作的时候耗时比较久,就会有一大堆请求卡在那,这种情况可能需要有一些异步提前刷新缓存,可以结合具体场景选择方式
+雪崩的情况是指大批量的 key 都一起过期了,击穿的放大版,大批量的请求都打到数据库上了,一方面有可能直接缓存不可用了,就需要用集群化高可用的缓存服务,然后对于实际使用中也可以使用本地缓存结合 redis 缓存,去提高可用性,再配合一些限流措施,然后就是缓存使用过程总的过期时间最好能加一些随机值,防止在同一时间过期而导致雪崩,结合互斥锁防止大量请求打到数据库。
]]>Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的
+public class Singleton1 {
+ // 首先,将构造方法变成私有的
+ private Singleton1() {};
+ // 创建私有静态实例,这样第一次使用的时候就会进行创建
+ private static Singleton instance = new Singleton1();
+
+ // 使用这个对象都是通过这个 getInstance 来获取
+ public static Singleton1 getInstance() {
+ return instance;
+ }
+ // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
+ // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
+ public static Date getDate(String mode) {return new Date();}
+}
+上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中getDate方法里说的问题,有时候并没有想那这个对象,但是因为我调用了这个类的静态方法,导致对象已经生成了,可能这也是饿汉模式名字的来由,不管三七二十一给你生成个单例就完事了,不管有没有用,但是这种个人觉得也没啥大问题,如果是面试的话最好说出来它的缺点
public class Singleton2 {
+ // 首先,也是先堵死 new Singleton() 这条路,将构造方法变成私有
+ private Singleton2() {}
+ // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
+ private static volatile Singleton2 instance = null;
+
+ private int m = 9;
+
+ public static Singleton getInstance() {
+ if (instance == null) {
+ // 加锁
+ synchronized (Singleton2.class) {
+ // 这一次判断也是必须的,不然会有并发问题
+ if (instance == null) {
+ instance = new Singleton2();
+ }
+ }
+ }
+ return instance;
+ }
+}
+这里容易错的有三点,理解了其实就比较好记了
+第一点,为啥不在 getInstance 上整个代码块加 synchronized,这个其实比较容易理解,就是锁的力度太大,性能太差了,这点其实也要去理解,可以举个夸张的例子,比如我一个电商的服务,如果为了避免一个人的订单出现问题,是不是可以从请求入口就把他锁住,到请求结束释放,那么里面做的事情都有保障,然而这显然不可能,因为我们想要这种竞态条件抢占资源的时间尽量减少,防止其他线程等待。
第二点,为啥synchronized之已经检查了 instance == null,还要在里面再检查一次,这个有个术语,叫 double check lock,但是为啥要这么做呢,其实很简单,想象当有两个线程,都过了第一步为空判断,这个时候只有一个线程能拿到这个锁,另一个线程就等待了,如果不再判断一次,那么第一个线程新建完对象释放锁之后,第二个线程又能拿到锁,再去创建一个对象。
第三点,为啥要volatile关键字,原先对它的理解是它修饰的变量在 JMM 中能及时将变量值写到主存中,但是它还有个很重要的作用,就是防止指令重排序,instance = new Singleton();这行代码其实在底层是分成三条指令执行的,第一条是在堆上申请了一块内存放这个对象,但是对象的字段啥的都还是默认值,第二条是设置对象的值,比如上面的 m 是 9,然后第三条是将这个对象和虚拟机栈上的指针建立引用关联,那么如果我不用volatile关键字,这三条指令就有可能出现重排,比如变成了 1-3-2 这种顺序,当执行完第二步时,有个线程来访问这个对象了,先判断是不是空,发现不是空的,就拿去直接用了,是不是就出现问题了,所以这个volatile也是不可缺少的
public class Singleton3 {
+
+ private Singleton3() {}
+ // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
+ private static class Holder {
+ private static Singleton3 instance = new Singleton3();
+ }
+ public static Singleton3 getInstance() {
+ return Holder.instance;
+ }
+}
+这个我个人感觉是饿汉模式的升级版,可以在调用getInstance的时候去实例化对象,也是比较推荐的
public enum Singleton {
+ INSTANCE;
+
+ public void doSomething(){
+ //todo doSomething
+ }
+}
+枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。
+]]>select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,+++
SELECT ... FROMis a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.
纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类
select * from table where ? lock in share mode;select * from table where ? for update;insert into table values (...);update table set ? where ?;delete from table where ?;除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了
其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了
select * from table1 where id = 1这种查询语句其实是不会加传说中的锁的,当然这里是指在 RR 或者 RC 隔离级别下,dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
+这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下
--+
SELECT ... FROMis a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。
纯粹的这种一致性读,实际读取的是快照,也就是基于 read view 的读取方式,除非当前隔离级别是SERIALIZABLE
但是对于以下几类
select * from table where ? lock in share mode;select * from table where ? for update;insert into table values (...);update table set ? where ?;delete from table where ?;除了第一条是 S 锁之外,其他都是 X 排他锁,这边在顺带下,S 锁表示共享锁, X 表示独占锁,同为 S 锁之间不冲突,S 与 X,X 与 S,X 与 X 之间都冲突,也就是加了前者,后者就加不上了
我们知道对于 RC 级别会出现幻读现象,对于 RR 级别不会出现,主要的区别是 RR 级别下对于以上的加锁读取都根据情况加上了 gap 锁,那么是不是 RR 级别下以上所有的都是要加 gap 锁呢,当然不是
举个例子,RR 事务隔离级别下,table1 有个主键id 字段select * from table1 where id = 10 for update
这条语句要加 gap 锁吗?
答案是不需要,这里其实算是我看了这么久的一点自己的理解,啥时候要加 gap 锁,判断的条件是根据我查询的数据是否会因为不加 gap 锁而出现数量的不一致,我上面这条查询语句,在什么情况下会出现查询结果数量不一致呢,只要在这条记录被更新或者删除的时候,有没有可能我第一次查出来一条,第二次变成两条了呢,不可能,因为是主键索引。
再变更下这个题的条件,当 id 不是主键,但是是唯一索引,这样需要怎么加锁,注意问题是怎么加锁,不是需不需要加 gap 锁,这里呢就是稍微延伸一下,把聚簇索引(主键索引)和二级索引带一下,当 id 不是主键,说明是个二级索引,但是它是唯一索引,体会下,首先对于 id = 10这个二级索引肯定要加锁,要不要锁 gap 呢,不用,因为是唯一索引,id = 10 只可能有这一条记录,然后呢,这样是不是就好了,还不行,因为啥,因为它是二级索引,对应的主键索引的记录才是真正的数据,万一被更新掉了咋办,所以在 id = 10 对应的主键索引上也需要加上锁(默认都是 record lock行锁),那主键索引上要不要加 gap 呢,也不用,也是精确定位到这一条记录
最后呢,当 id 不是主键,也不是唯一索引,只是个普通的索引,这里就需要大名鼎鼎的 gap 锁了,
是时候画个图了
其实核心的目的还是不让这个 id=10 的记录不会出现幻读,那么就需要在 id 这个索引上加上三个 gap 锁,主键索引上就不用了,在 id 索引上已经控制住了id = 10 不会出现幻读,主键索引上这两条对应的记录已经锁了,所以就这样 OK 了
然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
第一步,我们到 brew的目录下
cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
第二步看看它的提交历史
git log --follow icu4c.rb
+在 git log 的海洋中寻找,寻找它的(64版本)的身影
第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支
git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
+其实注意 commit id 旁边的红框,这个是有tag 的,可以直接
+git checkout icu4c-64
+PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本
brew reinstall ./icu4c.rb
+然后就是第五步,切换版本
+brew switch icu4c 64.2
+最后把分支切回来
+git checkout master
+是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法
+# zsh
+function hiicu64() {
+ local last_dir=$(pwd)
+
+ cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+ git checkout icu4c-4
+ brew reinstall ./icu4c.rb
+ brew switch icu4c 64.2
+ git checkout master
+
+ cd $last_dir
+}
+对应自己的版本改改版本号就可以了,非常好用。
+]]>这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。
+鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆
+一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆
+植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆
+对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。
+厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。
+]]>"main@1" prio=5 tid=0x1 nid=NA runnable
+ java.lang.Thread.State: RUNNABLE
+ at TreeDistance.treeDist(TreeDistance.java:64)
+ at TreeDistance.treeDist(TreeDistance.java:65)
+ at TreeDistance.treeDist(TreeDistance.java:65)
+ at TreeDistance.treeDist(TreeDistance.java:65)
+ at TreeDistance.main(TreeDistance.java:45)
+这就是我们主线程的堆栈信息了,main 表示这个线程名,prio表示优先级,默认是 5,tid 表示线程 id,nid 表示对应的系统线程,后面的runnable 表示目前线程状态,因为是被我打了断点,所以是就许状态,然后下面就是对应的线程栈内容了,在TreeDistance类的 treeDist方法中,对应的文件行数是 64 行。
这里使用 thread dump一般也不会是上面我截图代码里的这种代码量很少的,一般是大型项目,有时候跑着跑着没反应,又不知道跑到哪了,特别是一些刚接触的大项目或者需要定位一个大项目的一个疑难问题,一时没思路时,可以使用这个方法,个人觉得非常有帮助。
上面根据 org.springframework.boot.autoconfigure.EnableAutoConfiguration 获取的各个配置类,在通过反射加载就能得到一堆 JavaConfig配置类,然后再根据 ConditionalOnProperty等条件配置加载具体的 bean,大致就是这么个逻辑
-]]> -Spring Boot应用,在这个应用范围内,我的常规 bean 是单例的,意味着 getBean 的时候其实永远只会拿到那一个对象,那要怎么来写一个单例呢,首先就是传说中的饿汉模式,也是最简单的
-public class Singleton1 {
- // 首先,将构造方法变成私有的
- private Singleton1() {};
- // 创建私有静态实例,这样第一次使用的时候就会进行创建
- private static Singleton instance = new Singleton1();
-
- // 使用这个对象都是通过这个 getInstance 来获取
- public static Singleton1 getInstance() {
- return instance;
- }
- // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
- // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
- public static Date getDate(String mode) {return new Date();}
-}
-上面借鉴了一些代码,其实这是最基本,也不会错的方法,但是正如其中getDate方法里说的问题,有时候并没有想那这个对象,但是因为我调用了这个类的静态方法,导致对象已经生成了,可能这也是饿汉模式名字的来由,不管三七二十一给你生成个单例就完事了,不管有没有用,但是这种个人觉得也没啥大问题,如果是面试的话最好说出来它的缺点
public class Singleton2 {
- // 首先,也是先堵死 new Singleton() 这条路,将构造方法变成私有
- private Singleton2() {}
- // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
- private static volatile Singleton2 instance = null;
-
- private int m = 9;
-
- public static Singleton getInstance() {
- if (instance == null) {
- // 加锁
- synchronized (Singleton2.class) {
- // 这一次判断也是必须的,不然会有并发问题
- if (instance == null) {
- instance = new Singleton2();
- }
- }
- }
- return instance;
- }
-}
-这里容易错的有三点,理解了其实就比较好记了
-第一点,为啥不在 getInstance 上整个代码块加 synchronized,这个其实比较容易理解,就是锁的力度太大,性能太差了,这点其实也要去理解,可以举个夸张的例子,比如我一个电商的服务,如果为了避免一个人的订单出现问题,是不是可以从请求入口就把他锁住,到请求结束释放,那么里面做的事情都有保障,然而这显然不可能,因为我们想要这种竞态条件抢占资源的时间尽量减少,防止其他线程等待。
第二点,为啥synchronized之已经检查了 instance == null,还要在里面再检查一次,这个有个术语,叫 double check lock,但是为啥要这么做呢,其实很简单,想象当有两个线程,都过了第一步为空判断,这个时候只有一个线程能拿到这个锁,另一个线程就等待了,如果不再判断一次,那么第一个线程新建完对象释放锁之后,第二个线程又能拿到锁,再去创建一个对象。
第三点,为啥要volatile关键字,原先对它的理解是它修饰的变量在 JMM 中能及时将变量值写到主存中,但是它还有个很重要的作用,就是防止指令重排序,instance = new Singleton();这行代码其实在底层是分成三条指令执行的,第一条是在堆上申请了一块内存放这个对象,但是对象的字段啥的都还是默认值,第二条是设置对象的值,比如上面的 m 是 9,然后第三条是将这个对象和虚拟机栈上的指针建立引用关联,那么如果我不用volatile关键字,这三条指令就有可能出现重排,比如变成了 1-3-2 这种顺序,当执行完第二步时,有个线程来访问这个对象了,先判断是不是空,发现不是空的,就拿去直接用了,是不是就出现问题了,所以这个volatile也是不可缺少的
public class Singleton3 {
-
- private Singleton3() {}
- // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
- private static class Holder {
- private static Singleton3 instance = new Singleton3();
- }
- public static Singleton3 getInstance() {
- return Holder.instance;
- }
-}
-这个我个人感觉是饿汉模式的升级版,可以在调用getInstance的时候去实例化对象,也是比较推荐的
public enum Singleton {
- INSTANCE;
-
- public void doSomething(){
- //todo doSomething
- }
-}
-枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。
-]]>比如我们在代码中 new 一个 ThreadLocal,
-public static void main(String[] args) {
- ThreadLocal<Man> tl = new ThreadLocal<>();
-
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(2);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(tl.get());
- }).start();
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- tl.set(new Man());
- }).start();
- }
-
- static class Man {
- String name = "nick";
- }
-这里构造了两个线程,一个先往里设值,一个后从里取,运行看下结果,
知道这个用法的话肯定知道是取不到值的,只是具体的原理原来搞错了,我们来看下设值 set 方法
public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
-}
-写博客这会我才明白我原来咋会错得这么离谱,看到第一行代码 t 就是当前线程,然后第二行就是用这个线程去getMap,然后我是把这个当成从 map 里取值了,其实这里是
ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
-}
-获取 t 的 threadLocals 成员变量,那这个 threadLocals 又是啥呢
它其实是线程 Thread 中的一个类型是java.lang.ThreadLocal.ThreadLocalMap的成员变量
这是 ThreadLocal 的一个静态成员变量
static class ThreadLocalMap {
-
- /**
- * The entries in this hash map extend WeakReference, using
- * its main ref field as the key (which is always a
- * ThreadLocal object). Note that null keys (i.e. entry.get()
- * == null) mean that the key is no longer referenced, so the
- * entry can be expunged from table. Such entries are referred to
- * as "stale entries" in the code that follows.
- */
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
-
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- }
-全部代码有点长,只截取了一小部分,然后我们再回头来分析前面说的 set 过程,再 copy 下代码
-public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
-}
-获取到 map 以后呢,如果 map 不为空,就往 map 里 set,这里注意 key 是啥,其实是当前这个 ThreadLocal,这里就比较明白了究竟是啥结构,每个线程都会维护自身的 ThreadLocalMap,它是线程的一个成员变量,当创建 ThreadLocal 的时候,进行设值的时候其实是往这个 map 里以 ThreadLocal 作为 key,往里设 value。
-这里又要看下前面的 ThreadLocalMap 结构了,类似 HashMap,它有个 Entry 结构,在设置的时候会先包装成一个 Entry
-private void set(ThreadLocal<?> key, Object value) {
-
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
-
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
-
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
-
- if (k == key) {
- e.value = value;
- return;
- }
-
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
+# Failure analyzers
+org.springframework.boot.diagnostics.FailureAnalyzer=\
+org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
+org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
+org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
+org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
+org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
+org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
+org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer
- tab[i] = new Entry(key, value);
- int sz = ++size;
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
-}
-这里其实比较重要的就是前面的 Entry 的构造方法,Entry 是个 WeakReference 的子类,然后在构造方法里可以看到 key 会被包装成一个弱引用,这里为什么使用弱引用,其实是方便这个 key 被回收,如果前面的 ThreadLocal tl实例被设置成 null 了,如果这里是直接的强引用的话,就只能等到线程整个回收了,但是其实是弱引用也会有问题,主要是因为这个 value,如果在 ThreadLocal tl 被设置成 null 了,那么其实这个 value 就会没法被访问到,所以最好的操作还是在使用完了就 remove 掉
-]]>dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
-这是什么鬼啊,然后我去这个目录下看了下,已经都是libicui18n.67.dylib了,而且它没有把原来的版本保留下来,首先这个是个叫 icu4c是啥玩意,谷歌了一下
---ICU4C是ICU在C/C++平台下的版本, ICU(International Component for Unicode)是基于”IBM公共许可证”的,与开源组织合作研究的, 用于支持软件国际化的开源项目。ICU4C提供了C/C++平台强大的国际化开发能力,软件开发者几乎可以使用ICU4C解决任何国际化的问题,根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,必须一提的是,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。
-
然后首先想到的解决方案就是能不能我使用brew install icu4c@64来重装下原来的版本,发现不行,并木有,之前的做法就只能是去网上把 64 的下载下来,然后放到这个目录,比较麻烦不智能,虽然没抱着希望在谷歌着,不过这次竟然给我找到了一个我认为非常 nice 的解决方案,因为是在 Stack Overflow 找到的,本着写给像我这样的小小白看的,那就稍微翻译一下
第一步,我们到 brew的目录下
cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
-这个可以理解为是 maven 的 pom 文件,不过有很多不同之处,使用ruby 写的,然后一个文件对应一个组件或者软件,那我们看下有个叫icu4c.rb的文件,
第二步看看它的提交历史
git log --follow icu4c.rb
-在 git log 的海洋中寻找,寻找它的(64版本)的身影
第三步注意这三个红框,Stack Overflow 给出来的答案这一步是找到这个 commit id 直接切出一个新分支
git checkout -b icu4c-63 e7f0f10dc63b1dc1061d475f1a61d01b70ef2cb7
-其实注意 commit id 旁边的红框,这个是有tag 的,可以直接
-git checkout icu4c-64
-PS: 因为我的问题是出在 64 的问题,Stack Overflow 回答的是 63 的,反正是一样的解决方法
第四部,切回去之后我们就可以用 brew 提供的基于文件的安装命令来重新装上 64 版本
brew reinstall ./icu4c.rb
-然后就是第五步,切换版本
-brew switch icu4c 64.2
-最后把分支切回来
-git checkout master
-是不是感觉很厉害的解决方法,大佬还提供了一个更牛的,直接写个 zsh 方法
-# zsh
-function hiicu64() {
- local last_dir=$(pwd)
+# Template availability providers
+org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
+org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
+org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
- cd $(brew --prefix)/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
- git checkout icu4c-4
- brew reinstall ./icu4c.rb
- brew switch icu4c 64.2
- git checkout master
+# DataSource initializer detectors
+org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
+org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector
+
- cd $last_dir
-}
-对应自己的版本改改版本号就可以了,非常好用。
-]]>这里看上去是有点民国时期的建筑风格,部分像那种电视里的租界啥的,不过这次去的时候都在翻修,路一大半拦起来了,听导游说这里往里面走有个局口街,然后上次听前同事说厦门比较有名的就是沙茶面和海蛎煎,不出意料的不太爱吃,沙茶面比较普通,可能是没吃到正宗的,海蛎煎吃不惯,倒是有个大叔的沙茶里脊还不错,在局口街那,还有小哥在那拍,应该也算是个网红打卡点了,然后吃了个油条麻糍也还不错,总体如果是看建筑的话可能最近不是个好时间,个人也没这方面爱好,吃的话最好多打听打听沙茶面跟海蛎煎哪里正宗。如果不知道哪家好吃,也不爱看这类建筑的可以排个坑。
-鼓浪屿也是完全没啥概念,需要乘船过去,但是只要二十分钟,岛上没有机动车,基本都靠走,有几个比较有名的地方,菽庄花园,里面有钢琴博物馆,对这个感兴趣的可以去看看,旁边是沙滩还可以逛逛,然后有各种博物馆,风琴啥的,岛上最大的特色是巷子多,道听途说有三百多条小巷,还有几个网红打卡点,周杰伦晴天墙,还有个最美转角,都是挤满了人排队打卡拍照,不过如果不着急,慢慢悠悠逛逛还是不错的,比较推荐,推荐值☆☆
-一直读不对这个字,都是叫:那个曾什么垵,愧对语文老师,这里到算是意外之喜,鼓浪屿回来已经挺累了,不过由于比较饿(什么原因后面说),并且离住的地方不远,就过去逛了逛,东西还蛮好吃的,芒果挺便宜,一大杯才十块,无骨鸡爪很贵,不是特别爱,臭豆腐不错的,也不算很贵,这里想起来,那边八婆婆的豆乳烧仙草还不错的,去中山路那会喝了,来曾厝垵也买了,奶茶爱好者可以试试,含糖量应该很高,不爱甜食或者减肥的同学慎重考虑好了再尝试,晚上那边从牌坊出来,沿着环岛路挺多夜宵店什么的,非常推荐,推荐值☆☆☆☆
-植物园还是挺名副其实的,有热带植物,沙漠多肉,因为赶时间逛得不多,热带雨林植物那太多人了,都是在那拍照,而且我指的拍照都是拍人照,本身就很小的路,各种十八线网红,或者普通游客在那摆 pose 拍照,挺无语的;沙漠多肉比较惊喜,好多比人高的仙人掌,一大片的仙人球,很可恶的是好多大仙人掌上都有人刻字,越来越体会到,我们社会人多了,什么样的都有,而且不少;还看了下百花厅,但没什么特别的,可能赶时间比较着急,没仔细看,比较推荐,推荐值☆☆☆
-对这个其实比较排斥,主要是比较晚了,跑的有点远(我太懒了),一开始真的挺拉低体验感受的,上来个什么书法家,现场画马,卖画;不过后面的还算值回票价,主题是花木兰,空中动作应该很考验基本功,然后那些老外的飞轮还跳绳(不知道学名叫啥),动物那块不太忍心看,应该是吃了不少苦头,不过人都这样就往后点再心疼动物吧。
-厦门是个非常适合干饭人的地方,吃饭的地方大部分是差不多一桌菜十个左右就完了,而且上来就一大碗饭,一瓶雪碧一瓶可乐,对于经常是家里跟亲戚吃饭都得十几二十个菜的乡下人来说,不太吃得惯这样的🤦♂️,当然很有可能是我们预算不足,点的差。但是有一点是我回杭州深有感触的,感觉杭州司机的素质真的是跟厦门的司机差了比较多,杭州除非公交车停了,否则人行道很难看到主动让人的,当然这里拿厦门这个旅游城市来对比也不是很公平,不过这也是体现城市现代化文明水平的一个维度吧。
-]]>"main@1" prio=5 tid=0x1 nid=NA runnable
- java.lang.Thread.State: RUNNABLE
- at TreeDistance.treeDist(TreeDistance.java:64)
- at TreeDistance.treeDist(TreeDistance.java:65)
- at TreeDistance.treeDist(TreeDistance.java:65)
- at TreeDistance.treeDist(TreeDistance.java:65)
- at TreeDistance.main(TreeDistance.java:45)
-这就是我们主线程的堆栈信息了,main 表示这个线程名,prio表示优先级,默认是 5,tid 表示线程 id,nid 表示对应的系统线程,后面的runnable 表示目前线程状态,因为是被我打了断点,所以是就许状态,然后下面就是对应的线程栈内容了,在TreeDistance类的 treeDist方法中,对应的文件行数是 64 行。
这里使用 thread dump一般也不会是上面我截图代码里的这种代码量很少的,一般是大型项目,有时候跑着跑着没反应,又不知道跑到哪了,特别是一些刚接触的大项目或者需要定位一个大项目的一个疑难问题,一时没思路时,可以使用这个方法,个人觉得非常有帮助。
上面根据 org.springframework.boot.autoconfigure.EnableAutoConfiguration 获取的各个配置类,在通过反射加载就能得到一堆 JavaConfig配置类,然后再根据 ConditionalOnProperty等条件配置加载具体的 bean,大致就是这么个逻辑
]]>只是这么平淡的生活就有一些自己比较心烦纠结的,之前有提到过的交通,最近似乎又发现了一点,就真相总是让人跌破眼镜,以前觉得我可能是胆子比较小,所以会觉得怎么路上这些电瓶都是这么肆无忌惮的往我冲过来,后面慢慢有一种借用电视剧读心神探的概念,安全距离,觉得大部分人跟我一样,骑电瓶车什么的总还是有个安全距离,只是可能这个安全距离对于不同的人不一样,那些骑电瓶车的潜意识里的安全距离是非常短,所以经常会骑车离着你非常近才会刹车,但是这个安全距离理论最近又被推翻了,因为经历过几次电瓶车就是已经跟你有身体接触了,但是没到把人撞倒的程度,似乎这些骑电瓶车的觉得步行的行人在人行道上是空气,蹭一下也无所谓,反正不能挡我的路,总感觉要不是我在前面骑自行车太慢挡着电瓶车,不然他们都能起飞去干掉 F35 解放湾湾了;
-另一个问题应该是说我们交通规则普及的太少,虽然我们没有路权这个名词概念,但是其实是有这个优先级的,包括像杭州是以公交车在人行道礼让行人闻名的,其实这个文明的行为只限于人行道在直行路中间的,大部分在十字路口,右转的公交车很少会让直行人行道的,前提是直行的绿灯的时候,特别是像公交车这样,车身特别长,右转的时候会有比较大的死角,如果是公交车先转,行人或者自行车很容易被卷进去,非常危险的,私家车就更不用说了,反正右转即使人行道上人非常多要转的也是一秒都不等,所以我自己在开车的时候是尽量在右转的时候等人行道上的行人或者骑车的走完,因为总会觉得我是不是有点双标,骑车走路的时候希望开车的能按规则让我,自己开车的时候又想赶紧开走,所以在开车的时候尽量做到让行车和骑车的。
-还有个其实是写着写着想起来的,比如我骑车左转的时候,因为我是左转到对角那就到了,跟那些左转后要再直行的不一样,我们应该在学车的时候也学过,超车要从左边超,但是往往那些骑电瓶车的在左转的时候会从我右边超过来再往左边撇过去,如果留的空间大还好,有些电瓶车就是如果车头超过了就不管他的车屁股,如果我不减速,自行车就被刮倒了,可能的确是别人就不是人,只要不把你撞倒就无所谓,反正为了你自己不被撞倒你肯定会让的。
-]]>我的一些观点也在前面说了,恋爱到婚姻,即使物质没问题,经济没问题,也会有各种各样的问题,需要一起去解决,因为结婚就意味着需要相互扶持,而不是各取所需,可能我的要求比较高,后面男女主在分手后还一起住了一段时间,我原来还在想会不会通过这个方式让他们继续去磨合同步,只是我失望了,最后给个打分可能是 5 到 6 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。
-]]>转籍其实是很方便的,在交警 12123 App 上申请就行了,在转籍以后,需要去实地验车,验车的话,在支付宝-杭州交警生活号里进行预约,找就近的车管所就好,需要准备一些东西,首先是行驶证,机动车登记证书,身份证,居住证,还有车上需要准备的东西是要有三脚架和反光背心,反光背心是最近几个月开始要的,问过之前去验车的只需要三脚架就好了,预约好了的话建议是赶上班时间越早越好,不然过去排队时间要很久,而且人多了以后会很乱,各种插队,而且有很多都是汽车销售,一个销售带着一堆车,我们附近那个进去的小路没一会就堵满车,进去需要先排队,然后扫码,接着交资料,这两个都排着队,如果去晚了就要排很久的队,交完资料才是排队等验车,验车就是打开引擎盖,有人会帮忙拓印发动机车架号,然后验车的会各种检查一下,车里面,还有后备箱,建议车内整理干净点,后备箱不要放杂物,检验完了之后,需要把三脚架跟反光背心放在后备箱盖子上,人在旁边拍个照,然后需要把车牌遮住后再拍个车子的照片,再之后就是去把车牌卸了,这个多吐槽下,那边应该是本来那边师傅帮忙卸车牌,结果他就说是教我们拆,虽然也不算难,但是不排除师傅有在偷懒,完了之后就是把旧车牌交回去,然后需要在手机上(警察叔叔 App)提交各种资料,包括身份证,行驶证,机动车登记证书,提交了之后就等寄车牌过来了。
-这里面缺失的一个环节就是选号了,选号杭州有两个方式,一种就是根据交管局定期发布的选号号段,可以自定义拼 20 个号,在手机上的交警 12123 App 上可以三个一组的形式提交,如果有没被选走的,就可以预选到这个了,但是这种就是也需要有一定策略,最新出的号段能选中的概率大一点,然后数字全是 8,6 这种的肯定会一早就被选走,然后如果跟我一样可以提前选下尾号,因为尾号数字影响限号,我比较有可能周五回家,所以得避开 5,0 的,第二种就是 50 选一跟以前新车选号一样,就不介绍了。第一种选中了以后可以在前面交还旧车牌的时候填上等着寄过来了,因为我是第一种选中的,第二种也可以在手机上选,也在可以在交还车牌的时候现场选。
-总体过程其实是 LD 在各种查资料跟帮我跑来跑去,要不是 LD,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。
-]]>前面其实是看的太阳的后裔,跟 LD 一起看的,之前其实算是看过一点,但是没有看的很完整,并且很多剧情也忘了,只是这个我我可能看得更少一点,因为最开始的时候觉得男主应该是男二,可能对长得这样的男主并且是这样的人设有点失望,感觉不是特别像个特种兵,但是由于本来也比较火,而且 LD 比较喜欢就从这个开始看了,有两个点是比较想说的
-韩剧虽然被吐槽的很多,但是很多剧的质量,情节把控还是优于目前非常多国内剧的,相对来说剧情发展的前后承接不是那么硬凹出来的,而且人设都立得住,这个是非常重要的,很多国内剧怎么说呢,就是当爹的看起来就比儿子没大几岁,三四十岁的人去演一个十岁出头的小姑娘,除非容貌异常,比如刘晓庆这种,不然就会觉得导演在把我们观众当傻子。瞬间就没有想看下去的欲望了。
-再一点就是情节是大众都能接受度比较高的,现在有很多普遍会找一些新奇的视角,比如卖腐,想某某令,两部都叫某某令,这其实是一个点,延伸出去就是跟前面说的一点有点类似,xx 老祖,人看着就二三十,叫 xx 老祖,(喜欢的人轻喷哈)然后名字有一堆,同一个人物一会叫这个名字,一会又叫另一个名字,然后一堆死表情。
-因为今天有个特殊的事情发生,所以简短的写(shui)一篇了
-]]>只是这么平淡的生活就有一些自己比较心烦纠结的,之前有提到过的交通,最近似乎又发现了一点,就真相总是让人跌破眼镜,以前觉得我可能是胆子比较小,所以会觉得怎么路上这些电瓶都是这么肆无忌惮的往我冲过来,后面慢慢有一种借用电视剧读心神探的概念,安全距离,觉得大部分人跟我一样,骑电瓶车什么的总还是有个安全距离,只是可能这个安全距离对于不同的人不一样,那些骑电瓶车的潜意识里的安全距离是非常短,所以经常会骑车离着你非常近才会刹车,但是这个安全距离理论最近又被推翻了,因为经历过几次电瓶车就是已经跟你有身体接触了,但是没到把人撞倒的程度,似乎这些骑电瓶车的觉得步行的行人在人行道上是空气,蹭一下也无所谓,反正不能挡我的路,总感觉要不是我在前面骑自行车太慢挡着电瓶车,不然他们都能起飞去干掉 F35 解放湾湾了;
+另一个问题应该是说我们交通规则普及的太少,虽然我们没有路权这个名词概念,但是其实是有这个优先级的,包括像杭州是以公交车在人行道礼让行人闻名的,其实这个文明的行为只限于人行道在直行路中间的,大部分在十字路口,右转的公交车很少会让直行人行道的,前提是直行的绿灯的时候,特别是像公交车这样,车身特别长,右转的时候会有比较大的死角,如果是公交车先转,行人或者自行车很容易被卷进去,非常危险的,私家车就更不用说了,反正右转即使人行道上人非常多要转的也是一秒都不等,所以我自己在开车的时候是尽量在右转的时候等人行道上的行人或者骑车的走完,因为总会觉得我是不是有点双标,骑车走路的时候希望开车的能按规则让我,自己开车的时候又想赶紧开走,所以在开车的时候尽量做到让行车和骑车的。
+还有个其实是写着写着想起来的,比如我骑车左转的时候,因为我是左转到对角那就到了,跟那些左转后要再直行的不一样,我们应该在学车的时候也学过,超车要从左边超,但是往往那些骑电瓶车的在左转的时候会从我右边超过来再往左边撇过去,如果留的空间大还好,有些电瓶车就是如果车头超过了就不管他的车屁股,如果我不减速,自行车就被刮倒了,可能的确是别人就不是人,只要不把你撞倒就无所谓,反正为了你自己不被撞倒你肯定会让的。
+]]>缓存穿透是指当数据库中本身就不存在这个数据的时候,使用一般的缓存策略时访问不到缓存后就访问数据库,但是因为数据库也没数据,所以如果不做任何策略优化的话,这类数据就每次都会访问一次数据库,对数据库压力也会比较大。
-缓存击穿跟穿透比较类似的,都是访问缓存不在,然后去访问数据库,与穿透不一样的是击穿是在数据库中存在数据,但是可能由于第一次访问,或者缓存过期了,需要访问到数据库,这对于访问量小的情况其实算是个正常情况,但是随着请求量变高就会引发一些性能隐患。
-缓存雪崩就是击穿的大规模集群效应,当大量的缓存过期失效的时候,这些请求都是直接访问到数据库了,会对数据库造成很大的压力。
-对于以上三种场景也有一些比较常见的解决方案,但也不能说是万无一失的,需要随着业务去寻找合适的方案
-对于数据库中就没这个数据的时候,一种是可以对这个 key 设置下空值,即以一个特定的表示是数据库不存在的,这种情况需要合理地调整过期时间,当这个 key 在数据库中有数据了的话,也需要有策略去更新这个值,并且如果这类 key 非常多,这个方法就会不太合适,就可以使用第二种方法,就是布隆过滤器,bloom filter,前置一个布隆过滤器,当这个 key 在数据库不存在的话,先用布隆过滤器挡一道,如果不在的话就直接返回了,当然布隆过滤器不是绝对的准确的
-当一个 key 的缓存过期了,如果大量请求过来访问这个 key,请求都会落在数据库里,这个时候就可以使用一些类似于互斥锁的方式去让一个线程去访问数据库,更新缓存,但是这里其实也有个问题,就是如果是热点 key 其实这种方式也比较危险,万一更新失败,或者更新操作的时候耗时比较久,就会有一大堆请求卡在那,这种情况可能需要有一些异步提前刷新缓存,可以结合具体场景选择方式
-雪崩的情况是指大批量的 key 都一起过期了,击穿的放大版,大批量的请求都打到数据库上了,一方面有可能直接缓存不可用了,就需要用集群化高可用的缓存服务,然后对于实际使用中也可以使用本地缓存结合 redis 缓存,去提高可用性,再配合一些限流措施,然后就是缓存使用过程总的过期时间最好能加一些随机值,防止在同一时间过期而导致雪崩,结合互斥锁防止大量请求打到数据库。
+我的一些观点也在前面说了,恋爱到婚姻,即使物质没问题,经济没问题,也会有各种各样的问题,需要一起去解决,因为结婚就意味着需要相互扶持,而不是各取所需,可能我的要求比较高,后面男女主在分手后还一起住了一段时间,我原来还在想会不会通过这个方式让他们继续去磨合同步,只是我失望了,最后给个打分可能是 5 到 6 分吧,勉强及格,好的影视剧应该源于生活高于生活,这一部可能还比不上生活。
]]>前面其实是看的太阳的后裔,跟 LD 一起看的,之前其实算是看过一点,但是没有看的很完整,并且很多剧情也忘了,只是这个我我可能看得更少一点,因为最开始的时候觉得男主应该是男二,可能对长得这样的男主并且是这样的人设有点失望,感觉不是特别像个特种兵,但是由于本来也比较火,而且 LD 比较喜欢就从这个开始看了,有两个点是比较想说的
+韩剧虽然被吐槽的很多,但是很多剧的质量,情节把控还是优于目前非常多国内剧的,相对来说剧情发展的前后承接不是那么硬凹出来的,而且人设都立得住,这个是非常重要的,很多国内剧怎么说呢,就是当爹的看起来就比儿子没大几岁,三四十岁的人去演一个十岁出头的小姑娘,除非容貌异常,比如刘晓庆这种,不然就会觉得导演在把我们观众当傻子。瞬间就没有想看下去的欲望了。
+再一点就是情节是大众都能接受度比较高的,现在有很多普遍会找一些新奇的视角,比如卖腐,想某某令,两部都叫某某令,这其实是一个点,延伸出去就是跟前面说的一点有点类似,xx 老祖,人看着就二三十,叫 xx 老祖,(喜欢的人轻喷哈)然后名字有一堆,同一个人物一会叫这个名字,一会又叫另一个名字,然后一堆死表情。
+因为今天有个特殊的事情发生,所以简短的写(shui)一篇了
+]]>转籍其实是很方便的,在交警 12123 App 上申请就行了,在转籍以后,需要去实地验车,验车的话,在支付宝-杭州交警生活号里进行预约,找就近的车管所就好,需要准备一些东西,首先是行驶证,机动车登记证书,身份证,居住证,还有车上需要准备的东西是要有三脚架和反光背心,反光背心是最近几个月开始要的,问过之前去验车的只需要三脚架就好了,预约好了的话建议是赶上班时间越早越好,不然过去排队时间要很久,而且人多了以后会很乱,各种插队,而且有很多都是汽车销售,一个销售带着一堆车,我们附近那个进去的小路没一会就堵满车,进去需要先排队,然后扫码,接着交资料,这两个都排着队,如果去晚了就要排很久的队,交完资料才是排队等验车,验车就是打开引擎盖,有人会帮忙拓印发动机车架号,然后验车的会各种检查一下,车里面,还有后备箱,建议车内整理干净点,后备箱不要放杂物,检验完了之后,需要把三脚架跟反光背心放在后备箱盖子上,人在旁边拍个照,然后需要把车牌遮住后再拍个车子的照片,再之后就是去把车牌卸了,这个多吐槽下,那边应该是本来那边师傅帮忙卸车牌,结果他就说是教我们拆,虽然也不算难,但是不排除师傅有在偷懒,完了之后就是把旧车牌交回去,然后需要在手机上(警察叔叔 App)提交各种资料,包括身份证,行驶证,机动车登记证书,提交了之后就等寄车牌过来了。
+这里面缺失的一个环节就是选号了,选号杭州有两个方式,一种就是根据交管局定期发布的选号号段,可以自定义拼 20 个号,在手机上的交警 12123 App 上可以三个一组的形式提交,如果有没被选走的,就可以预选到这个了,但是这种就是也需要有一定策略,最新出的号段能选中的概率大一点,然后数字全是 8,6 这种的肯定会一早就被选走,然后如果跟我一样可以提前选下尾号,因为尾号数字影响限号,我比较有可能周五回家,所以得避开 5,0 的,第二种就是 50 选一跟以前新车选号一样,就不介绍了。第一种选中了以后可以在前面交还旧车牌的时候填上等着寄过来了,因为我是第一种选中的,第二种也可以在手机上选,也在可以在交还车牌的时候现场选。
+总体过程其实是 LD 在各种查资料跟帮我跑来跑去,要不是 LD,估计在交管局那边我就懵逼了,各种插队,而且车子开着车子,也不能随便跑,所以建议办这个的时候有个人一起比较好。
+]]>比如我们在代码中 new 一个 ThreadLocal,
+public static void main(String[] args) {
+ ThreadLocal<Man> tl = new ThreadLocal<>();
+
+ new Thread(() -> {
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(tl.get());
+ }).start();
+ new Thread(() -> {
+ try {
+ TimeUnit.SECONDS.sleep(1);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ tl.set(new Man());
+ }).start();
+ }
+
+ static class Man {
+ String name = "nick";
+ }
+这里构造了两个线程,一个先往里设值,一个后从里取,运行看下结果,
知道这个用法的话肯定知道是取不到值的,只是具体的原理原来搞错了,我们来看下设值 set 方法
public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
+}
+写博客这会我才明白我原来咋会错得这么离谱,看到第一行代码 t 就是当前线程,然后第二行就是用这个线程去getMap,然后我是把这个当成从 map 里取值了,其实这里是
ThreadLocalMap getMap(Thread t) {
+ return t.threadLocals;
+}
+获取 t 的 threadLocals 成员变量,那这个 threadLocals 又是啥呢
它其实是线程 Thread 中的一个类型是java.lang.ThreadLocal.ThreadLocalMap的成员变量
这是 ThreadLocal 的一个静态成员变量
static class ThreadLocalMap {
+
+ /**
+ * The entries in this hash map extend WeakReference, using
+ * its main ref field as the key (which is always a
+ * ThreadLocal object). Note that null keys (i.e. entry.get()
+ * == null) mean that the key is no longer referenced, so the
+ * entry can be expunged from table. Such entries are referred to
+ * as "stale entries" in the code that follows.
+ */
+ static class Entry extends WeakReference<ThreadLocal<?>> {
+ /** The value associated with this ThreadLocal. */
+ Object value;
+
+ Entry(ThreadLocal<?> k, Object v) {
+ super(k);
+ value = v;
+ }
+ }
+ }
+全部代码有点长,只截取了一小部分,然后我们再回头来分析前面说的 set 过程,再 copy 下代码
+public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
+}
+获取到 map 以后呢,如果 map 不为空,就往 map 里 set,这里注意 key 是啥,其实是当前这个 ThreadLocal,这里就比较明白了究竟是啥结构,每个线程都会维护自身的 ThreadLocalMap,它是线程的一个成员变量,当创建 ThreadLocal 的时候,进行设值的时候其实是往这个 map 里以 ThreadLocal 作为 key,往里设 value。
+这里又要看下前面的 ThreadLocalMap 结构了,类似 HashMap,它有个 Entry 结构,在设置的时候会先包装成一个 Entry
+private void set(ThreadLocal<?> key, Object value) {
+
+ // We don't use a fast path as with get() because it is at
+ // least as common to use set() to create new entries as
+ // it is to replace existing ones, in which case, a fast
+ // path would fail more often than not.
+
+ Entry[] tab = table;
+ int len = tab.length;
+ int i = key.threadLocalHashCode & (len-1);
+
+ for (Entry e = tab[i];
+ e != null;
+ e = tab[i = nextIndex(i, len)]) {
+ ThreadLocal<?> k = e.get();
+
+ if (k == key) {
+ e.value = value;
+ return;
+ }
+
+ if (k == null) {
+ replaceStaleEntry(key, value, i);
+ return;
+ }
+ }
+
+ tab[i] = new Entry(key, value);
+ int sz = ++size;
+ if (!cleanSomeSlots(i, sz) && sz >= threshold)
+ rehash();
+}
+这里其实比较重要的就是前面的 Entry 的构造方法,Entry 是个 WeakReference 的子类,然后在构造方法里可以看到 key 会被包装成一个弱引用,这里为什么使用弱引用,其实是方便这个 key 被回收,如果前面的 ThreadLocal tl实例被设置成 null 了,如果这里是直接的强引用的话,就只能等到线程整个回收了,但是其实是弱引用也会有问题,主要是因为这个 value,如果在 ThreadLocal tl 被设置成 null 了,那么其实这个 value 就会没法被访问到,所以最好的操作还是在使用完了就 remove 掉
+]]>class RenameTest extends TestCase
+ 聊聊部分公交车的设计bug
+ /2021/12/05/%E8%81%8A%E8%81%8A%E9%83%A8%E5%88%86%E5%85%AC%E4%BA%A4%E8%BD%A6%E7%9A%84%E8%AE%BE%E8%AE%A1bug/
+ 今天惯例坐公交回住的地方,不小心撞了头,原因是我们想坐倒数第二排,然后LD 走在我后面,我就走到最后一排中间等着,但是最后一排是高一截的,等 LD 坐进去以后,我就往前走,结果撞到了车顶的扶手杆子的一端,差点撞昏了去,这里我觉得其实杆子长度应该短一点,不然从最后一排出来,还是有比较大概率因为没注意看而撞到头,特别是没注意看的情况,发力其实会比较大,一头撞上就会像我这样,眼前一黑,又痛得要死。
还有一点就是座位设计了,先来看个图
![]()
图里大致画了两条线,因为可能是轮胎还是什么原因,后排中间会有那么大的突起,但是看两条红线可以发现,靠近过道的座位边缘跟地面突起的边缘不是一样宽的,这样导致的结果就是坐着的时候有一个脚没地儿搁,要不就得侧着斜着坐,或者就是一个脚悬空,短程的可能还好,路程远一点还是比较难受的,特别是像我现在这样,大腿外侧有点难受的情况,就会更难受。
虽然说这两个点,基本是屁用没有,但是我也是在自己这个博客说说,也当是个树洞了。
+]]>
+
+ 生活
+
+
+ 生活
+ 公交
+ 杭州
+
+ composer require phpunit/phpunitphpunit, 前面包就是通过 composer init 创建,装完依赖后就可以把自动加载代码生成下 composer dump-autoload.
+├── composer.json
+├── composer.lock
+├── oldfile.txt
+├── phpunit.xml
+├── src
+│ └── Rename.php
+└── tests
+ └── RenameTest.php
+
+2 directories, 6 files
+src/是源码,tests/是放的单测,比较重要的是phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
+<phpunit colors="true" bootstrap="vendor/autoload.php">
+ <testsuites>
+ <testsuite name="php-rename">
+ <directory>./tests/</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>
+其中bootstrap就是需要把依赖包的自动加载入口配上,因为这个作为一个package,也会指出命名空间
然后就是testsuite的路径,源码中
<?php
+namespace Nicksxs\PhpRename;
+
+class Rename
{
- public function setUp(): void
+ public static function renameSingleFile($file, $newFileName): bool
{
- var_dump("setUp");
+ if(!is_file($file)) {
+ echo "it's not a file";
+ return false;
+ }
+ $fileInfo = pathinfo($file);
+ return rename($file, $fileInfo["dirname"] . DIRECTORY_SEPARATOR . $newFileName . "." . $fileInfo["extension"]);
}
+}
+就是一个简单的重命名
然后test代码是这样,
<?php
- public function test1()
+// require_once 'vendor/autoload.php';
+
+use PHPUnit\Framework\TestCase;
+use Nicksxs\PhpRename\Rename;
+use function PHPUnit\Framework\assertEquals;
+
+class RenameTest extends TestCase
+{
+ public function setUp() :void
{
- var_dump("test1");
- assertEquals(1, 1);
+ $myfile = fopen(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "w") or die("Unable to open file!");
+ $txt = "file test1\n";
+ fwrite($myfile, $txt);
+ fclose($myfile);
}
-
- public function test2()
+ public function testRename()
{
- var_dump("test2");
- assertEquals(1, 1);
+ Rename::renameSingleFile(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "newfile");
+ assertEquals(is_file(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt"), true);
}
protected function tearDown(): void
{
- var_dump("tearDown");
+ unlink(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt");
}
-}
-因为我是想写个重命名的小工具,希望通过setUp和tearDown做一些文件初始化和清理工作,但是我把两个case的初始化跟清理工作写到了单个setUp和tearDown中,这样就出现了异常的错误
通过上面的示例代码,可以看到执行结果
setUp 跟 tearDown 就是初始化跟结束清理的,但是注意如果不指明 __DIR__ ,待会的目录就会在执行 vendor/bin/phpunit 下面,
或者也可以指定在一个 tmp/ 目录下
最后就可以通过vendor/bin/phpunit 来执行测试
执行结果
❯ vendor/bin/phpunit
PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
-.string(5) "setUp"
-string(5) "test1"
-string(8) "tearDown"
-. 2 / 2 (100%)string(5) "setUp"
-string(5) "test2"
-string(8) "tearDown"
-
+. 1 / 1 (100%)
Time: 00:00.005, Memory: 6.00 MB
-OK (2 tests, 2 assertions)
-其实就是很简单的会在每个test方法前后都执行setUp和tearDown
那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。
+现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。
+说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。
+]]>那位大哥,骑电瓶车一前一后带着两个娃,在非机动车道靠右边行驶,肉眼估计是在我右前方大概十几米的距离,不知道是小孩不舒服了还是啥,想下来还是就在跟他爹玩耍,我算是比较谨慎骑车的,看到这种情况已经准备好捏刹车了,但是也没想到这个娃这么神,差不多能并排四五辆电瓶车的非机动车道,直接从他爸的车下来跑到了非机动车道的最左边,前面我铺垫了电瓶车 25 码,换算一下大概 1 秒能前进 7 米,我是直接把刹车捏死了,才勉强避免撞上这个小孩,并且当时的情况本来我左后方有另一个大哥是想从我左边超过去,因为我刹车了他也赶紧刹车。
-现在我们做个假设,假如我刹车不够及时,撞上了这个小孩,会是啥后果呢,小孩人没事还好,即使没事也免不了大吵一架,说我骑车不看前面,然后去医院做检查,负责医药费,如果是有点啥伤了,这事估计是没完了,我是心里一阵后怕。
-说实话是张口快骂人了,“怎么带小孩的”,结果那大哥竟然还是那套话术,“你们骑车不会慢点的啊,说一下就好了啊,用得着这么说吗”,我是真的被这位的逻辑给打败了,还好是想超我车那大哥刹住车了,他要是刹不住呢,把我撞了我怪谁?这不是追尾事件,是 zhizhang 大哥的小孩鬼探头,下个电瓶车就下车,下来就往另一边跑,我们尽力刹车没撞到这小孩,说他没管好小孩这大哥还觉得自己委屈了?结果我倒是想骂脏话了,结果我左后方的的大哥就跟他说“你这么教小孩教得真好,你真厉害”,果然在中国还是不能好好说话,阴阳怪气才是王道,我前面也说了真的是后怕,为什么我从头到尾都没有说这个小孩不对,我是觉得这个年纪的小孩(估摸着也就五六岁或者再大个一两岁)这种安全意识应该是要父母和学校老师一起教育培养的,在路上不能这么随便乱跑,即使别人撞了他,别人有责任,那小孩的生理伤痛和心理伤害,父母也肯定要心疼的吧,另外对我们来说前面也说了,真的撞到了我们也是很难受的,这个社会里真的是自私自利的人太多了,平时让外卖小哥送爬下楼梯送上来外卖都觉得挺抱歉的,每次的接过来都说谢谢,人家也不容易,换在有些人身上大概会觉得自己花了钱就是大爷,给我送上来是必须的。
-]]>composer require phpunit/phpunitphpunit, 前面包就是通过 composer init 创建,装完依赖后就可以把自动加载代码生成下 composer dump-autoload.
-├── composer.json
-├── composer.lock
-├── oldfile.txt
-├── phpunit.xml
-├── src
-│ └── Rename.php
-└── tests
- └── RenameTest.php
-
-2 directories, 6 files
-src/是源码,tests/是放的单测,比较重要的是phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
-<phpunit colors="true" bootstrap="vendor/autoload.php">
- <testsuites>
- <testsuite name="php-rename">
- <directory>./tests/</directory>
- </testsuite>
- </testsuites>
-</phpunit>
-其中bootstrap就是需要把依赖包的自动加载入口配上,因为这个作为一个package,也会指出命名空间
然后就是testsuite的路径,源码中
<?php
-namespace Nicksxs\PhpRename;
-
-class Rename
+ 记录下 phpunit 的入门使用方法之setUp和tearDown
+ /2022/10/23/%E8%AE%B0%E5%BD%95%E4%B8%8B-phpunit-%E7%9A%84%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E4%B9%8BsetUp%E5%92%8CtearDown/
+ 可能是太久没写单测了,写个单测发现不符合预期,后来验证下才反应过来
我们来看下demo
+class RenameTest extends TestCase
{
- public static function renameSingleFile($file, $newFileName): bool
+ public function setUp(): void
{
- if(!is_file($file)) {
- echo "it's not a file";
- return false;
- }
- $fileInfo = pathinfo($file);
- return rename($file, $fileInfo["dirname"] . DIRECTORY_SEPARATOR . $newFileName . "." . $fileInfo["extension"]);
+ var_dump("setUp");
}
-}
-就是一个简单的重命名
然后test代码是这样,
<?php
-
-// require_once 'vendor/autoload.php';
-
-use PHPUnit\Framework\TestCase;
-use Nicksxs\PhpRename\Rename;
-use function PHPUnit\Framework\assertEquals;
-class RenameTest extends TestCase
-{
- public function setUp() :void
+ public function test1()
{
- $myfile = fopen(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "w") or die("Unable to open file!");
- $txt = "file test1\n";
- fwrite($myfile, $txt);
- fclose($myfile);
+ var_dump("test1");
+ assertEquals(1, 1);
}
- public function testRename()
+
+ public function test2()
{
- Rename::renameSingleFile(__DIR__ . DIRECTORY_SEPARATOR . "oldfile.txt", "newfile");
- assertEquals(is_file(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt"), true);
+ var_dump("test2");
+ assertEquals(1, 1);
}
protected function tearDown(): void
{
- unlink(__DIR__ . DIRECTORY_SEPARATOR . "newfile.txt");
+ var_dump("tearDown");
}
-}
-setUp 跟 tearDown 就是初始化跟结束清理的,但是注意如果不指明 __DIR__ ,待会的目录就会在执行 vendor/bin/phpunit 下面,
或者也可以指定在一个 tmp/ 目录下
最后就可以通过vendor/bin/phpunit 来执行测试
执行结果
因为我是想写个重命名的小工具,希望通过setUp和tearDown做一些文件初始化和清理工作,但是我把两个case的初始化跟清理工作写到了单个setUp和tearDown中,这样就出现了异常的错误
通过上面的示例代码,可以看到执行结果
❯ vendor/bin/phpunit
PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
-. 1 / 1 (100%)
+.string(5) "setUp"
+string(5) "test1"
+string(8) "tearDown"
+. 2 / 2 (100%)string(5) "setUp"
+string(5) "test2"
+string(8) "tearDown"
+
Time: 00:00.005, Memory: 6.00 MB
-OK (1 test, 1 assertion)
+OK (2 tests, 2 assertions)
+其实就是很简单的会在每个test方法前后都执行setUp和tearDown

-


+


