你好呀,我是歪歪。

这篇文章带大家盘一下 LFU 这个玩意。

为什么突然想起聊聊这个东西呢,因为前段时间有个读者给我扔过来一个链接:

我一看,好家伙,这不是我亲爱的老朋友,Dubbo 同学嘛。

点进去一看,就是一个关于 LFU 缓存的 BUG:

https://github.com/apache/dubbo/issues/10085

你知道的,我就喜欢盘一点开源项目的 BUG,看看有没有机会,捡捡漏什么的。在我辉煌的“捡漏”历中,曾经最浓墨重彩的一笔是在 Redisson 里面,发现作者在重构代码的时候,误把某个方法的计数,从默认值为 0,改成了为 1。

这个 BUG 会直接导致 Redission 锁失去可重入的性质。我发现后,源码都没下载,直接在网页上就改回去了:

我以为这可能就是巅峰了。

但是直到我遇到了 Dubbo 的这个 LFUCache 的 BUG,它的修复方案,只是需要交换两行代码的顺序就完事儿了,更加简单。

到底怎么回事呢,我带你盘一把。

首先,刚刚提到 BUG,因为这一次针对 LFU 实现的优化提交:

https://github.com/apache/dubbo/pull/7967

通过链接我们知道,这次提交的目的是优化 LFUCache 这个类,使其能通过 frequency 获取到对应的 key,然后删除空缓存。

但是带了个内存泄露的 BUG 上去,那么这个 BUG 是怎么修复的呢?

直接给对应的提交给回滚掉了:

但是,回滚回来的这一份代码,我个人觉得也是有问题的,使用起来,有点不得劲。

在为你解析 Dubbo 的 LFU 实现的问题之前,我必须要先把 LFU 这个东西的思路给你盘明白了,你才能丝滑入戏。

LRU vs LFU

在说 LFU 之前,我先给简单提一句它的好兄弟:LRU,Least Recently Used,最近最少使用。

LRU 这个算法的思想就是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。所以,当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

听描述你也知道了,它是一种淘汰算法。

如果说 LRU 是 Easy 模式的话,那么把中间的字母从 R(Recently) 变成 F(Frequently),即 LFU ,那就是 hard 模式了。

如果你不认识 Frequently 没关系,毕竟这是一个英语专八的词汇。

我,英语八级半,我可以教你:

这是一个 adv,副词,是指在句子中表示行为或状态特征的词,用以修饰动词、形容词、其他副词或全句,表示时间、地点、程度、方式等概念。

在 LFU 里面,表示的就是频率,它翻译过来就是:最不经常使用策略。

LRU 淘汰数据的时候,只看数据在缓存里面待的时间长短这个维度。

而 LFU 在缓存满了,需要淘汰数据的时候,看的是数据的访问次数,被访问次数越多的,就越不容易被淘汰。

但是呢,有的数据的访问次数可能是相同的。

怎么处理呢?

如果访问次数相同,那么再考虑数据在缓存里面待的时间长短这个维度。

也就是说 LFU 算法,先看访问次数,如果次数一致,再看缓存时间。

给你举个具体的例子。

假设我们的缓存容量为 3,按照下列数据顺序进行访问:

如果按照 LRU 算法进行数据淘汰,那么十次访问的结果如下:

十次访问结束后,缓存中剩下的是 b,c,d 这三个元素。

你仔细嗦一下,你有没有觉得有一丝丝不对劲?

十次访问中元素 a 被访问了 5 次,说明 a 是一个经常要用到的东西,结果按照 LRU 算法,最后元素 a 被淘汰了?

如果按照 LFU 算法,最后留在缓存中的三个元素应该是 b,c,a。

这样看来,LFU 比 LRU 更加合理,更加巴适。

好的,题目描述完毕了。假设,要我们实现一个 LFUCache:

class LFUCache {

    public LFUCache(int capacity) {

    }
    
    public int get(int key) {

    }
    
    public void put(int key, int value) {

    }
}

那么思路应该是怎样的呢?

一个双向链表

如果是我去面试,在完全没有接触过 LFU 算法之前,面试官出了这么一道题,让我硬想,我能想到的方案也只能是下面这样的。

因为前面我们分析了,这个玩意既需要有频次,又需要有时间顺序。

我们就可以搞个链表,先按照频次排序,频次一样的,再按照时间排序。

因为这个过程中我们需要删除节点,所以为了效率,我们使用双向链表。

还是假设我们的缓存容量为 3,还是用刚刚那组数据进行演示。

我们把频次定义为 freq,那么前三次访问结束后,即这三个请求访问结束后:

每个元素的频次,即 freq 都是 1。所以链表里面应该是这样的:

由于我们的容量就是 3,此时已经满了。

那我问你一个问题:如果此时来任意一个不是 a 的元素,谁会被踢出缓存?

就这问题,你还思考个毛啊,这不是一目了然的事情吗?

对于目前三个元素来说,value=a 是频次相同的情况下,最久没有被访问到的元素,所以它就是 head 节点的下一个元素,随时等着被淘汰。

但是你说巧不巧,

接着过来的请求就是 value=a:

当这个请求过来的时候,链表中的 value=a 的节点的频率(freq)就变成了2。

此时,它的频率最高,最不应该被淘汰,a 元素完成了自我救赎!

因此,链表变成了下面这个样子,没有任何元素被淘汰了。

链表变化的部分,我用不同于白色的颜色(我色弱不知道这是个啥颜色,蓝色吗?)标注出来:

接着连续来了三个 value=a 的请求:

此时的链表变化就集中在 value=a 这个节点的频率(freq)上。

为了让你能丝滑跟上,我把每次的 freq 变化都给你画出来。这行为,实锤了,妥妥的暖男作者:

接着,这个 b 请求过来了:

b 节点的 freq 从 1 变成了 2,节点的位置也发生了变化:

然后,c 请求过来:

这个时候就要特别注意了:

你说这个时候会发生什么事情?

链表中的 c 当前的访问频率是 1,当这个 c 请求过来后,那么链表中的 c 的频率就会变成 2。

你说巧不巧,此时,value=b 节点的频率也是 2。

撞车了,那么你说,这个时候怎么办?

前面说了:频率一样的时候,看时间。

value=c 的节点是正在被访问的,所以要淘汰也应该淘汰之前被访问的 value=b 的节点。

此时的链表,就应该是这样的:

然后,最后一个请求过来了:

d 元素,之前没有在链表里面出现过,而此时链表的容量也满了。

那么刺激的就來了,该进行淘汰了,谁会被“优化”掉呢?

一看链表,哦,head 的下一个节点是 value=b:

然后把 value=d 的节点插入:

最终,所有请求完毕。

留在缓存中的是 d,c,a 这三个元素。

最后,汇个总,整体的流程就是这样的:

当然,这里只是展示了链表的变化。

前面说了,这是缓存淘汰策略,缓存嘛,大家都懂,是一个 key-value 键值对。

所以前面的元素 a,b,c 啥的,其实对应的是我们放的 key-value 键值对。也就是应该还有一个 HashMap 来存储 key 和链表节点的映射关系。

这个点比较简单,用脚趾头都能想到,我也就不展开来说了。

按照上面这个思路,你慢慢的写代码,应该是能写出来的。

上面这个双链表的方案,就是扣着脑壳硬想,大部分人能直接想到的方案。

但是,面试官如果真的是问这个问题的话,当你给出这个回答之后,他肯定会追问:有没有时间复杂度为 O(1) 的解决方案呢?

双哈希表

如果我们要拿出时间复杂度为 O(1) 的解法,那就得细细的分析了,不能扣着脑壳硬想了。

我们先分析一下需求。

第一点:我们需要根据 key 查询其对应的 value。

前面说了,这是一个缓存淘汰策略,缓存嘛,用脚趾头都能想到,用 HashMap 存储 key-value 键值对。

查询时间复杂度为 O(1),满足。

第二点:每当我们操作一个 key 的时候,不论是查询还是新增,都需要维护这个 key 的频次,记作 freq。

因为我们需要频繁的操作 key 对应的 freq,频繁地执行把 freq 拿出来进行加一的操作。

获取,加一,再放回去。来,请你大声的告诉我,用什么数据结构?

是不是还得再来一个 HashMap 存储 key 和 freq 的对应关系?

第三点:如果缓存里面放不下了,需要淘汰数据的时候,把 freq 最小的 key 删除掉。

注意啊,上面这句话,看黑板,我再强调一下:把 freq 最小的 key 删除掉。

freq 最小?

我们怎么知道哪个 key 的 freq 最小呢?

前面说了,我们有一个 HashMap 存储 key 和 freq 的对应关系。我们肯定是可以遍历这个 HashMap,来获取到 freq 最小的 key。

但是啊,朋友们,遍历出现了,那么时间复杂度还会是 O(1) 吗?

那怎么办呢?

注意啊,高潮来了,一学就废,一点就破。

我们可以搞个变量来记录这个最小的 freq 啊,记为 minFreq,在对缓存操作的过程中持续的对其进行维护,不就行了?

现在我们有最小频次(minFreq)了,需要获取到这个最小频次对应的 key,时间复杂度得为 O(1)。

来,朋友,请你大声的告诉我,你又想起了什么数据结构?

是不是又想到了 HashMap?

好了,我们现在有三个 HashMap 了,给大家介绍一下:

一个存储 key 和 value 的 HashMap,即HashMap<key,value>。
一个存储 key 和 freq 的 HashMap,即HashMap<key,freq>。
一个存储 freq 和 key 的 HashMap,即HashMap<freq,key>。

它们每个都是各司其职,目的都是为了时间复杂度为 O(1)。

但是我们可以把前两个 HashMap 合并一下。

我们弄一个对象,对象里面包含两个属性分别是 value、freq。

假设这个对象叫做 Node,它就是这样的,频次默认为 1:

class Node {
    int value;
    int freq = 1;
    //构造函数省略
}

那么现在我们就可以把前面两个 HashMap ,替换为一个了,即 HashMap<key,Node>。

同理,我们可以在 Node 里面再加入一个 key 属性:

class Node {
    int key;
    int value;
    int freq = 1;
    //构造函数省略
}

因为 Node 里面包含了 key,所以可以把第三个 HashMap<freq,key> 替换为 HashMap<freq,Node>。

当我们有了封装了 key、value、freq 属性的 Node 对象之后,我们的三个 HashMap 就变成了两个:

一个存储 key 和 Node 的 HashMap,即 HashMap<key,Node>
一个存储 freq 和 Node 的 HashMap,即 HashMap<freq,Node>

你捋一捋这个思路,是不是非常的清晰,有了清晰的思路,去写代码是不是就事半功倍了。

好,现在我告诉你,到这一步,我们还差一个逻辑,而且这个逻辑非常的重要,你现在先别着急往下看,先再回顾一下目前整个推理的过程和最后的思路,想一想到底还差啥?

...

...

...

...

到这一步,我们还差了一个非常关键的信息没有补全,就是下面这一个点。

第四点:可能有多个 key 具有相同的最小的 freq,此时移除这一批数据在缓存中待的时间最长的那个元素。

在这个需求里面,我们需要通过 freq 查找 Node,那么操作的就是 HashMap<freq,Node> 这个哈希表。

上面说[多个 key 具有相同的最小的 freq],也就是说通过 minFreq ,是可以查询到多个 Node 的。

所以HashMap<freq,Node> 这个哈希表,应该是这样的:HashMap<freq,集合>。

这个能想明白吧?

一个坑位下,挂了一串节点。

此时的问题就变成了:我们应该用什么集合来装这个 Node 对象呢?

不慌,我们又接着分析嘛。

先理一下这个集合需要满足什么条件。

我们通过 minFreq 获取这个集合的时候,也就是队列满了,要从这个集合中删除数据的时候

首先,需要删除 Node 的时候。

因为这个集合里面装的是访问频次一样的数据,那么希望这批数据能有时序,这样可以快速的删除待的时间最久的 Node。

有序,有时序,能快速查找删除待的时间最久的 key。

LinkedList,双向链表,可以满足这个需求吧?

另外还有一种大多数情况是一个 Node 被访问的时候,它的频次必然就会加一。

所以还要考虑访问 Node 的时候。

比如下面这个例子:

假设最小访问频次,minFreq=5,而 5 这个坑位对应了 3 个 Node 对象。

此时,我要访问 value=b 的对象,那么该对象就会从 minFreq=5 的 value 中移走。

然后频次加一,即 5+1=6。

加入到 minFreq=6 的 value 集合中,变成下面这个样子:

也就是说我们得支持任意 node 的快速删除。

LinkedList 不支持任意 node 的快速删除,这玩意需要遍历啊。

当然,你可以自己去手撸一个符合要求的 MySelfLinkedList。

但是,在 Java 集合类中,其实有一个满足上面说的有序的、支持快速删除的集合。

那就是 LinkedHashSet,它是一个基于 LinkedHashMap 实现的有序的、去重集合列表。

底层还是一个 Map,Map 针对指定元素的删除,O(1)。

所以,HashMap<freq,集合>,就是HashMap<freq,LinkedHashSet>。

总结一下。

我们需要两个 HashMap,分别是

  • HashMap<key,Node>
  • HashMap<freq,LinkedHashSet>

然后还需要维护一个最小访问频次,minFreq。

哦,对了,还得来一个参数记录缓存支持的最大容量,capacity。

然后,没了。

有的小伙伴肯定要问了:你倒是给我一份代码啊?

这些分析出来了,代码自己慢慢就撸出来了,这一份代码应该就是绝大部分面试官问 LFU 的时候,想要看到的代码了。

另外,关于 LFU 出现在面试环节,我突然想起一个段子,我觉得还有一丝道理:

面试官想要,我会出个两数之和,如果我不想要你,我就让你写LFU。

我这里主要带大家梳理思路,思路清晰后再去写代码,就算面试的时候没有写出 bug free 的代码,也基本上八九不离十了。

所以具体的代码实现,我这里就不提供了,网上一搜多的很,关键是把思路弄清楚。

这玩意就像是你工作,关键的是把需求梳理明白,然后想想代码大概是怎么样的。

至于具体去写的时候,到处粘一粘,也不是不可以。再或者,把设计文档写出来了,代码落地就让小弟们照着你的思路去干就行了。

应该把工作精力放在更值得花费的地方:比如 battle 需求、写文档、写周报、写 PPT、舔领导啥的...

Dubbo 的 LFU

到这里,你应该是懂了 LFU 到底是个啥玩意了。

现在我带你看看 Dubbo 里面的这个 LFU,它的实现方案并没有采取我们前面分析出来的方案。

它另辟蹊径,搞了一个新花样出来。它是怎么实现的呢?我给你盘一下。

Dubbo 是在 2.7.7 版本之后支持的 LFU,所以如果你要在本地测试一把的话,需要引入这个版本之后的 Maven 依赖。

我这里直接是把 Dubbo 源码拉下来,然后看的是 Master 分支的代码。

首先看一下它的数据结构:

它有一个 Map,是存放的 key 和 Node 之间的映射关系。然后还有一个 freqTable 字段,它的数据结构是数组,我们并没有看到一个叫做 minFreq 的字段。

当我的 LFUCache 这样定义的时候:

new LFUCache<>(5, 0.2f);

含义是这个缓存容量是 5,当满了之后,“优化” 5*0.2 个对象,即从缓存中踢出 1 个对象。

通过 Debug 模式看,它的结构是这样的:

我们先主要关注 freqTable 字段。

在下面标号为 ① 的地方,表示它是一个长度为 capacity+1 的数组,即长度为 6,且每个位置都 new 了一个 CacheDeque 对象。

标号为 ② 的地方,完成了一个单向链表的维护动作:

freqTable 具体拿来是干啥用的呢?

用下面这三行代码举个例子:

cache.put("why", 18);
cache.get("why");
cache.get("why");

当第一行执行完成之后,why 这个元素被放到了 freqTable 的第一个坑位,即数组的 index=0 里面:

当第二行执行完成之后,why 这个元素被放到了 freqTable 的第二个坑位里面:

当第三行执行完成之后,why 这个元素被放到了 freqTable 的第三个坑位里面:

有没有点感觉了?

freqTable 这个数组,每个坑位就是对应不同的频次,所以,我给你搞个图:

比如 index=3 位置放的 d 和 c,含义就是 d 和 c 在缓存里面被访问的频次是 4 次。

但是,d 和 c 谁待的时间更长呢?

我也不知道,所以得看看源码里面删除元素的时候,是移走 last 还是 first,移走谁,谁就在这个频率下待的时间最长。

答案就藏在它的驱逐方法里面:

org.apache.dubbo.common.utils.LFUCache#proceedEviction

从数组的第一个坑位开始遍历数组,如果这个坑位下挂的有链表,然后开始不断的移除头结点,直到驱逐指定个数的元素。

移除头结点,所以,d 是在这个频次中待的时间最长的。

基于这个图片,假设现在队列满了,那么接下来,肯定是要把 why 这个节点给“优化”了:

这就是 Dubbo 里面 LFU 实现的最核心的思想,很巧妙啊,基于数组的顺序,来表示元素被访问的频次。

但是,细细的嗦一下味道之后,你有没有想过这样一个问题,当我访问缓存中的元素的次数大于 freqTable 数组的长度的时候,会出现什么神奇的现象?

我还是给你用代码示意:

虽然 mx 元素的访问次数比 why 元素的次数多得多,但是这两个元素最后都落在了 freqTable 数组的最后一个坑位中。

也就是会出现这样的场景:

好,我问你一个问题:假设,在这样的情况下,要淘汰元素了,谁会被淘汰?

肯定是按照头结点的顺序开始淘汰,也就是 why 这个节点。

接下来注意了,我再问一个问题:假设此时 why 有幸再次被访问了一下,然后才来一个新的元素,触发了淘汰策略,谁会被淘汰?

why 会变成这个坑位下的链表中的 last 节点,从而躲避过下一次淘汰。mx 元素被淘汰了。

这玩意突然就从 LFU,变成了一个 LRU。在同一个实现里面,既有 LFU 又有 LRU,这玩意,知识都学杂了啊。

我就问你 6 不 6?

所以,这就是 Dubbo 的 LFU 算法实现的一个弊端,这个弊端是由于它的设计理念和数据结构决定的,如果想要避免这个问题,我觉得有点麻烦,得推翻从来。

所以我这里也只是给你描述一下有这个问题的存在。

然后,关于 Dubbo 的 LFU 的实现,还有另外一个神奇的东西,我觉得这纯纯的就是 BUG 了。

我还是给你搞一个测试用例:

@Test
void testPutAndGet() {
    LFUCache<String, Integer> cache = new LFUCache<>(5, 0.2f);
    for (int i = 0; i < 5; i++) {
        cache.put(i + "", i);
        cache.get(i + "");
    }
    cache.put("why", 18);
    Integer why = cache.get("why");
    System.out.println("why = " + why);
}

一个容量为 5 的 LFUCache,我先放五个元素进去,每个元素 put 之后再 get 一下,所以每个元素的频率变成了 2。

然后,此时我放了一个 why 进去,然后在取出来的时候, why 没了。

你这啥缓存啊,我刚刚放进去的东西,等着马上就要用呢,结果获取的时候没了,这不是 BUG 是啥?

问题就出在它的 put 方法中:

标号为 ① 的地方往缓存里面放,放置完毕之后,判断是否会触发淘汰机制,然后在标号 ② 的地方删除元素。

前面说了,淘汰策略,proceedEviction 方法,是从 freqTable 数组的第一个坑位开始遍历数组,如果这个坑位下挂的有链表,然后开始不断的移除头结点,直到驱逐指定个数的元素。

所以,在刚刚我的示例代码中,why 元素刚刚放进去,它的频率是 1,放在了 freqTable 数组的第 0 个位置。

放完之后,一看,触发淘汰机制了,让 proceedEviction 方法看看是谁应该被淘汰了。

你说,这不是赶巧了嘛,直接又把 why 给淘汰了,因为它的访问频率最低。

所以,立马去获取 “why” 的时候,获取不到。

这个地方的逻辑就是有问题的。

不应该采取“先放新元素,再判断容量,如果满了,触发淘汰机制”的实现方案。

而应该采取“先判断容量,如果满了,再触发淘汰机制,最后再放新元素的”方案。

也就是我需要把这两行代码换个位置:

再次执行测试案例,就没有问题了:

诶,你说这是什么?

这不就是为开源项目贡献源码的好机会吗?

于是...

https://github.com/apache/dubbo/pull/11538

关于提 pr,有一个小细节,我悄悄的告诉你:合不合并什么的不重要,重要的是全英文提问,哪怕是中文一键翻译过来的,也要贴上去,逼格要拉满,档次要上去...

至于最后,如果没合并,说明我考虑不周,又是新的素材。

如果合并了,简历上又可以写:熟练使用 Dubbo,并为其贡献过源码。

到时候,我脸皮再厚一点,还可以写成:阅读过 Apache 顶级开源项目源码,多次为其贡献过核心源码,并写过系列博文,加深学习印象...

不能再说了,剩下的就靠自己悟了!

最后,文章就到这里了,如果对你有一丝丝的帮助,帮我点个免费的赞,不过分吧?

关于我在学习LFU的时候,在开源项目捡了个漏这件事。的更多相关文章

  1. [转载]Android开发者必须深入学习的10个应用开源项目

    [转载]Android开发者必须深入学习的10个应用开源项目 原文地址:Android开发者必须深入学习的10个应用开源项目(http://blog.sina.com.cn/s/blog_7b8a63 ...

  2. Android开发者必须深入学习的10个应用开源项目

    Android 开发又将带来新一轮热潮,很多开发者都投入到这个浪潮中去了,创造了许许多多相当优秀的应用.其中也有许许多多的开发者提供了应用开源项 目,贡献出他们的智慧和创造力.学习开源代码是掌握技术的 ...

  3. 一些值得学习和借鉴的.Net 开源项目

    1.DotNetFramework .NET Reference Source 发布了 beta 版,可以在线浏览 .NET Framework 4.5.1 的源代码,并且可以通过配置,在 Visua ...

  4. 一些值得深入学习和借鉴的 .Net 开源项目

    1.DotNetFramework  .NET Reference Source 发布了 beta 版,可以在线浏览 .NET Framework 4.5.1 的源代码,并且可以通过配置,在 Visu ...

  5. Net-Snmp工具(学习SNMP的工具,开源项目)简单使用

    https://blog.csdn.net/mrzhangzifu/article/details/77882371 Net-Snmp工具的安装与配置  操作系统:Ubuntu16.4  软件版本:n ...

  6. 转: 学习开源项目的若干建议(infoq)

    转: http://www.infoq.com/cn/news/2014/04/learn-open-source 学习开源项目的若干建议 作者 崔康 发布于 2014年4月11日 | 注意:GTLC ...

  7. 腾讯数据安全专家谈联邦学习开源项目FATE:通往隐私保护理想未来的桥梁

    数据孤岛.数据隐私以及数据安全,是目前人工智能和云计算在大规模产业化应用过程中绕不开的“三座大山”. “联邦学习”作为新一代的人工智能算法,能在数据不出本地的情况下,实现共同建模,提升AI模型的效果, ...

  8. Android 开源项目及其学习

    Android 系统研究:http://blog.csdn.net/luoshengyang/article/details/8923485 Android 腾讯技术人员博客 http://hukai ...

  9. 学习Coding-iOS开源项目日志(五)

    继续,接着前面第四篇<学习Coding-iOS开源项目日志(四)>讲解Coding-iOS开源项目. 前 言:作为初级程序员,想要提高自己的水平,其中一个有效的学习方法就是学习别人好的项目 ...

  10. 学习Coding-iOS开源项目日志(一)

    前言:作为初级程序员,想要提高自己的水平,其中一个有效的学习方法就是学习别人好的项目.本篇开始会陆续更新本人对github上开源的一个很不错的项目的一点点学习积累.也就是,探究着别人写的源码,我学到了 ...

随机推荐

  1. Kubeadm搭建kubernetes集群

    Kubeadm搭建kubernetes集群 环境说明 | 角色 | ip | 操作系统 |组件 | | – | – | – | | master | 192.168.203.100 |centos8 ...

  2. Seata 1.5.2 源码学习(Client端)

    在上一篇中通过阅读Seata服务端的代码,我们了解到TC是如何处理来自客户端的请求的,今天这一篇一起来了解一下客户端是如何处理TC发过来的请求的.要想搞清楚这一点,还得从GlobalTransacti ...

  3. <四>理解空间配置器allocator, 优化STL 中的Vector

    .在上一节我们实现的 MyVector存在哪些问题? 问题1 现在有Student类 class Student{ public: Student(){cout<<"构造Stud ...

  4. 第2-4-4章 规则引擎Drools规则属性-业务规则管理系统-组件化-中台

    目录 5. 规则属性 5.1 enabled属性 5.2 dialect属性 5.3 salience属性 5.4 no-loop属性 5.5 activation-group属性 5.6 agend ...

  5. 解决manjaro无法连接github问题

    修改/etc/hosts文件 1.查看连接ip地址: https://ping.chinaz.com 2.在hosts文件下增加: vim /etc/hosts 需要管理员权限 140.82.113. ...

  6. docker给已存在的容器添加或修改端口映射

    简述: 这几天研究了一下docker, 发现建立完一个容器后不能增加端口映射了,因为 docker run -p 有 -p 参数,但是 docker start 没有 -p 参数,让我很苦恼,无奈谷歌 ...

  7. GO开发工具litelDE的安装与使用

    1.MinGW的下载与安装 地址:http://sourceforge.net/projects/mingw/ 下载安装 D:\Program Files\MinGW 然后打开D:\Program F ...

  8. php+apache环境搭建

    [先安装apache] apache快速安装:https://www.cnblogs.com/brad93/p/16718104.html PHP安装教程参考:https://www.cnblogs. ...

  9. Gepetto:使用chatGPT来对函数功能进行分析并重命名变量的IDA插件

    最近OpenAI的chatGPT很火,chatGPT是一个大型的语言模型,能够生成人类语言的文本,主要用于对话式的问答和聊天,以及模拟人类的对话行为 有关chatGPT的介绍就不多赘述了,相关内容很多 ...

  10. Python怎么引入不同的库?

    怎么引入不同的库? 在线安装库 1)pip install 模块名 2)国内源: 清华:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:http://mirr ...