首页 文章 精选 留言 我的

精选列表

搜索[本地知识库],共10000篇文章
优秀的个人博客,低调大师

秒建 wiki 知识库的开源项目,构建私人知识网络

不知道有没有人和我一样,觉得自建的东西是互联网上的“自留地”、私人空间,有一种自己的一亩三分地随心所欲的痛快。 比如自建的博客想写什么随笔就写什么,不用取悦读者可以自娱自乐;再比如自建的 wiki 有不会的知识点就可以直接记录,不用担心被嘲笑低级。抛开共建这块不聊,Wiki 不同于博客的随性,记录的内容更注重知识点和分类,可以用来构建自己的知识网络。 如果把博客比作“日记本”,那 wiki 就是“笔记本”它用来记录知识点,方便用时查阅和更新,有清晰的目录而且一个知识点还可以关联到其它知识点,逐步拓展成“百科全书”。 一、介绍 在于积累,还不能忘记梳理。 今天,我们要介绍的开源项目是专门用来构建 wiki 平台,助你梳理知识点的 wiki.js 地址:https://github.com/requarks/wiki 它是一款轻量级、功能强大的 wiki 开源项目,拥有评论、Markdown 编辑器、图片上传、标签、全局搜索、协同编辑、编辑历史、用户管理、谷歌分析等功能,而且支持高度自定义。 用到的技术栈也不同于老旧的 wiki 系统,它采用了 Node.js、PostgreSQL、Vue.js、Docker 等技术。基于 Docker 实现的一键部署,颇有 WordPress 之风,不要太爽! 重点是支持中文,而且界面简洁还不失美感,这点足以让它在众多同类项目中脱颖而出。 看到这儿,你是不是手痒了呢?下面就和我一起来让它跑起来吧! 二、安装 开源项目成功的必要因素之一就是有详细易懂的文档,而安装说明又是重中之重。 Wiki.js 官方文档提供了多种部署方法,包括:Linux、macOS、Windows、Docker、k8s 等,涵盖了几乎所有可能性,十分全面。 下面我就介绍其中最快捷和通用的一种,即基于 Docker 的 Docker Compose 部署。 Tips:如果你不懂 Docker 建议跟着这里逐步执行 下面我将主要介绍 Linux 下的安装步骤,其它系统有桌面版不再赘述。 如果你机器上有 Docker 仅需两步即可完成安装。 第一步,安装 docker-compose: 1、下载 curl -L https://get.daocloud.io/docker/compose/releases/download/v2.4.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose 2、加执行权限 $ sudo chmod +x /usr/local/bin/docker-compose 3、创建快捷方式 $ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose 至此,你就可以在任何地方使用 docker-compose 命令了。 第二步,运行 docker-compose: 1、创建配置文件 docker-compose.yml 内容如下: 整个项目分为 数据库 和 项目代码 两部分,与之对应的是 pg 容器 和 wiki 容器。 version: "3" services: db: container_name: pg image: postgres:11-alpine environment: POSTGRES_DB: wiki POSTGRES_PASSWORD: wikijsrocks POSTGRES_USER: wikijs logging: driver: "none" restart: unless-stopped volumes: - db-data:/var/lib/postgresql/data wiki: container_name: wiki image: ghcr.io/requarks/wiki:2 depends_on: - db environment: DB_TYPE: postgres DB_HOST: db DB_PORT: 5432 DB_USER: wikijs DB_PASS: wikijsrocks DB_NAME: wiki restart: unless-stopped ports: - "8001:3000" volumes: db-data: 2、在配置所在的目录下,执行命令: 运行:docker-compose up -d 查看容器:docker ps 停止:docker-compose down 最后,如果你想开启 HTTPS 的话,我这里推荐用 Caddy 服务器。没用过没关系,我们写过介绍使用 Caddy 的文章特别简单。 Caddyfile 的配置内容如下: 8001 端口对应的是上面 wiki 容器的 ports 端口映射 域名 { reverse_proxy 127.0.0.1:8001 } 执行 caddy start 启动 Caddy 服务器,浏览器中访问对应的域名,网站初始化的引导界面,就会出现在你的面前了。 至此,以上就是 wiki.js 安装的全过程,你跑起来了吗? 三、瑕不掩瑜 Wiki.js 并不是十全十美的,虽然我只是刚上手,但还是发现了一些美中不足: 第一次访问加载速度较慢 虽然 wiki.js 更新积极、提交频繁,但目前它还不支持自定义主题 对中文搜索不友好,默认不支持中文搜索,需要采用 ES 但这样就不再轻量,或者采用 pg 插件让 pg 支持中文分词 中文翻译覆盖率并不像官网展示的 100%,管理后台里还是有未翻译的地方 但是瑕不掩瑜,它基本上实现了我对 wiki 想要的所有功能。而且总好过自己从头实现一个 wiki 系统吧,后面我会用 wiki.js 做一个新的网站: https://cheatsheet.store/ 等我玩顺手了搞通上面的问题就去给它提 PR 做贡献,期待更强大的 wiki.js! 四、最后 知识需要融会贯通。 知识本是杂乱无章的,需要通过实践经验,让它们建立联系,变得井然有序,才会得心应手,释放出强大的创造力。 最后,用 wiki.js 构建你的知识网络,梳理已有的知识不断推陈出新,让它在你寻求更高突破的路上,助你一臂之力! 更多讲解开源项目的文章尽在:https://github.com/HelloGitHub-Team/Article

优秀的个人博客,低调大师

Python知识库大全让你一键“配齐”

红红火火恍恍惚惚,借着猪年的喜庆,小猪佩琦又在中国火了一把。2019年,有没有为自己设定一个目标呢,比如学好python,让自己更有竞争力。今天给大家深度挖掘社区的大宝库,这里的python资源你绝对值得拥有! 2018社区top10文章干货:Python学习笔记:开始Python编程大佬程序员给小白整理出的详细Python爬虫学习路线,机不可失!Python的5种传参姿势,两分钟就能了解Python 10大谬论,你可能对Python存在的一些误解!Python程序员的30个常见错误Python编辑器你选哪个?我选PyCharm边玩游戏边学 Python ,编程竟如此有趣 !python 赋值语句学习Python,怎能不懂点PEP呢?R语言和 Python —— 一个错误的分裂除了AI,你不该忽视Python在这4大领域的应用! 学习

优秀的个人博客,低调大师

LTR:应用于电商智能客服领域知识库搜索的实践

关键词:搜索、机器学习、学习排序、Learning to Rank(LTR) 1:背景 搜索引擎排序(Ranking)的优化是搜索领域中普遍遇到的问题,通常会涉及到很多的排序策略,传统的排序方法一般通过构造相关度函数,然后按照相关度进行排序。影响相关度的因素很多,也有很多经典算法模型来满足这一需求,但是对于传统的排序方法,有很多特征信息的情况下,单一的函数或者模型很难融合很多参数,也容易出现过拟合的现象,而机器学习方法很容易融合多种特征,能一定程度的解决稀疏、过拟合等问题。因此,以机器学习的理论基础来解决ranking问题便形成了Learning to Rank(学习排序、LTR)的方法。 Learning to Rank是在机器学习领域中有监督学习(Supervised Learning)的排序方法,如今已经被广泛应用于搜索

优秀的个人博客,低调大师

本地缓存 Caffeine 中的时间轮(TimeWheel)是什么?

我们详细介绍了 Caffeine 缓存添加元素和读取元素的流程,并详细解析了配置固定元素数量驱逐策略的实现原理。在本文中我们将主要介绍 配置元素过期时间策略的实现原理,补全 Caffeine 对元素管理的机制。在创建有过期时间策略的 Caffeine 缓存时,它提供了三种不同的方法,分别为 expireAfterAccess, expireAfterWrite 和 expireAfter,前两者的元素过期机制非常简单:通过遍历队列中的元素(expireAfterAccess 遍历的是窗口区、试用区和保护区队列,expireAfterWrite 有专用的写顺序队列 WriteOrderDeque),并用当前时间减去元素的最后访问时间(或写入时间)的结果值和配置的时间作对比,如果超过配置的时间,则认为元素过期。而 expireAfter 为自定义过期策略,使用到了时间轮 TimeWheel。它的实现相对复杂,在源码中相关的方法都会包含 Variable 命名(变量;可变的),如 expiresVariable。本文以如下源码创建自定义过期策略的缓存来了解 Caffeine 中的 TimeWheel 机制,它创建的缓存类型为 SSA,表示 Key 和 Value 均为强引用且配置了自定义过期策略: public class TestReadSourceCode { @Test public void doReadTimeWheel() { Cache<String, String> cache2 = Caffeine.newBuilder() // .expireAfterAccess(5, TimeUnit.SECONDS) // .expireAfterWrite(5, TimeUnit.SECONDS) .expireAfter(new Expiry<>() { @Override public long expireAfterCreate(Object key, Object value, long currentTime) { // 指定过期时间为 Long.MAX_VALUE 则不会过期 if ("key0".equals(key)) { return Long.MAX_VALUE; } // 设置条目在创建后 5 秒过期 return TimeUnit.SECONDS.toNanos(5); } // 以下两个过期时间指定为默认 duration 不过期 @Override public long expireAfterUpdate(Object key, Object value, long currentTime, @NonNegative long currentDuration) { return currentDuration; } @Override public long expireAfterRead(Object key, Object value, long currentTime, @NonNegative long currentDuration) { return currentDuration; } }) .build(); cache2.put("key2", "value2"); System.out.println(cache2.getIfPresent("key2")); try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(cache2.getIfPresent("key2")); } } 在前文中我们提到过,Caffeine 中 maintenance 方法负责缓存的维护,包括执行缓存的驱逐,所以我们便以这个方法作为起点去探究时间过期策略的执行: abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> { @GuardedBy("evictionLock") void maintenance(@Nullable Runnable task) { setDrainStatusRelease(PROCESSING_TO_IDLE); try { drainReadBuffer(); drainWriteBuffer(); if (task != null) { task.run(); } drainKeyReferences(); drainValueReferences(); // 元素过期策略执行 expireEntries(); evictEntries(); climb(); } finally { if ((drainStatusOpaque() != PROCESSING_TO_IDLE) || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) { setDrainStatusOpaque(REQUIRED); } } } } 在其中我们能发现 expireEntries 元素过期策略的执行方法,该方法的源码如下所示: abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> { @GuardedBy("evictionLock") void expireEntries() { // 获取当前时间 long now = expirationTicker().read(); // 基于访问和写后的过期策略 expireAfterAccessEntries(now); expireAfterWriteEntries(now); // *主要关注*:执行自定义过期策略 expireVariableEntries(now); // Pacer 用于调度和执行定时任务,创建 Caffeine 缓存时可通过 scheduler 方法来配置 // 默认为 null Pacer pacer = pacer(); if (pacer != null) { long delay = getExpirationDelay(now); if (delay == Long.MAX_VALUE) { pacer.cancel(); } else { pacer.schedule(executor, drainBuffersTask, now, delay); } } } } 在自定义的过期策略中,我们便能够发现 TimeWheel 的身影,并调用了它的 TimeWheel#advance 方法: abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> { @GuardedBy("evictionLock") void expireVariableEntries(long now) { if (expiresVariable()) { timerWheel().advance(this, now); } } } 在深入 TimeWheel 的源码前,我们先通过源码注释了解下它的作用。TimeWheel 是 Caffeine 缓存中定义的类,它的类注释如下写道: 一个 分层的 时间轮,能够以O(1)的时间复杂度添加、删除和触发过期事件。过期事件的执行被推迟到 maintenance 维护方法中的 TimeWheel#advance 逻辑中。 A hierarchical timer wheel to add, remove, and fire expiration events in amortized O(1) time. The expiration events are deferred until the timer is advanced, which is performed as part of the cache's maintenance cycle. 可见上文中我们看见的 TimeWheel#advance 方法是用来执行元素过期事件的。在这段注释中提到了 分层(hierarchical) 的概念,注释中还有一段内容对这个特性进行了描述: 时间轮将计时器事件存储在循环缓冲区的桶中。Bucket 表示粗略的时间跨度,例如一分钟,并使用一个双向链表来记录事件。时间轮按层次结构(秒、分钟、小时、天)构建,这样当时间轮旋转时,在遥远的未来安排的事件会级联到较低的桶中。它允许在O(1)的时间复杂度下添加、删除和过期事件,在整个 Bucket 中的元素都会发生过期,同时有大部分元素过期的特殊情况由时间轮的轮换分摊。 A timer wheel stores timer events in buckets on a circular buffer. A bucket represents a coarse time span, e.g. one minute, and holds a doubly-linked list of events. The wheels are structured in a hierarchy (seconds, minutes, hours, days) so that events scheduled in the distant future are cascaded to lower buckets when the wheels rotate. This allows for events to be added, removed, and expired in O(1) time, where expiration occurs for the entire bucket, and the penalty of cascading is amortized by the rotations. 大概能够推测,它的“分层”概念指的是根据事件的过期时间将这些事件放在不同的“层”去管理,秒级别的在一层,分钟级别的在一层等等。那么接下来我们根据它的注释内容,详细探究一下 TimeWheel 是如何实现这种机制的。 constructor 我们先来看它的构造方法: final class TimerWheel<K, V> implements Iterable<Node<K, V>> { // 定义了 5 个桶,每个桶的容量分别为 64、64、32、4、1 static final int[] BUCKETS = {64, 64, 32, 4, 1}; final Node<K, V>[][] wheel; TimerWheel() { wheel = new Node[BUCKETS.length][]; for (int i = 0; i < wheel.length; i++) { wheel[i] = new Node[BUCKETS[i]]; for (int j = 0; j < wheel[i].length; j++) { wheel[i][j] = new Sentinel<>(); } } } // 定义 Sentinel 内部类,创建对象表示双向链表的哨兵节点 static final class Sentinel<K, V> extends Node<K, V> { Node<K, V> prev; Node<K, V> next; Sentinel() { prev = next = this; } } } 它会创建如下所示的二维数组,每行都作为一个桶(Bucket),根据 BUCKETS 数组中定义的容量,每行桶的容量分别为 64、64、32、4、1,每个桶中的初始元素是创建 Sentinel 为节点的双向链表,如下所示(其中...表示图中省略了31个桶): 它为什么会创建一个内部类 Sentinel 并将其作为桶中的初始元素呢?在《算法导论》中讲解链表的章节提到过这个方法:双向链表中没有元素时也不为 null,而是创建一个哨兵节点(Sentinel),它不存储任何数据,只是为了方便链表的操作,减少代码中的判空逻辑,而在此处将其命名为 Sentinel,表示采用这个方法,又同时提高了代码的可读性。 schedule 现在它的数据结构我们已经有了基本的了解,那么究竟什么时候会向其中添加元素呢?在前文中我们提到过,向 Caffeine 缓存中 put 元素时会注册 AddTask 任务,任务中有一段逻辑会调用 TimeWheel#schedule 方法向其中添加元素: final class AddTask implements Runnable { @Override @GuardedBy("evictionLock") @SuppressWarnings("FutureReturnValueIgnored") public void run() { // ... if (isAlive) { // ... // 如果自定义时间策略,则执行 schedule 方法 if (expiresVariable()) { // node 为添加的节点 timerWheel().schedule(node); } } // ... } } 我们来看看 schedule 方法,根据的它的入参可以发现上文注释中提到的“过期事件”便是 Node 节点本身: final class TimerWheel<K, V> implements Iterable<Node<K, V>> { static final long[] SPANS = { // 1073741824L 2^30 1.07s ceilingPowerOfTwo(TimeUnit.SECONDS.toNanos(1)), // 68719476736L 2^36 1.14m ceilingPowerOfTwo(TimeUnit.MINUTES.toNanos(1)), // 36028797018963968L 2^45 1.22h ceilingPowerOfTwo(TimeUnit.HOURS.toNanos(1)), // 864691128455135232L 2^50 1.63d ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 5629499534213120000L 2^52 6.5d BUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), BUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), }; // Long.numberOfTrailingZeros 表示尾随 0 的数量 static final long[] SHIFT = { // 30 Long.numberOfTrailingZeros(SPANS[0]), // 36 Long.numberOfTrailingZeros(SPANS[1]), // 45 Long.numberOfTrailingZeros(SPANS[2]), // 50 Long.numberOfTrailingZeros(SPANS[3]), // 52 Long.numberOfTrailingZeros(SPANS[4]), }; final Node<K, V>[][] wheel; long nanos; public void schedule(Node<K, V> node) { // 在 wheel 中找到对应的桶,node.getVariableTime() 获取的是元素的“过期时间” Node<K, V> sentinel = findBucket(node.getVariableTime()); // 将该节点添加到桶中 link(sentinel, node); } // 初次添加时,time 为元素的“过期时间” Node<K, V> findBucket(long time) { // 计算 duration 持续时间(有效期) long duration = time - nanos; // length 为 4 int length = wheel.length - 1; for (int i = 0; i < length; i++) { // 注意这里它是将持续时间和 SPANS[i + 1] 作比较,如果小于则认为该节点在这个“层级”中 if (duration < SPANS[i + 1]) { // tick 指钟表的滴答声,用于表示它所在层级的偏移量,举个例子: // 如果 duration < SPANS[1] 表示秒级别的时间跨度,则右移 SHIFT[0] 30 位,SPANS[0] 为 2^30 对应 1.07s,右移 30 位足以将 time 表示为秒级别的时间跨度 long ticks = (time >>> SHIFT[i]); // 2进制数-1 的位与运算计算出节点在该层级的实际索引位置 int index = (int) (ticks & (wheel[i].length - 1)); return wheel[i][index]; } } // 如果遍历完所有层级都没有合适的,那么将其放在最后一层 return wheel[length][0]; } // 双向链表的尾插法添加元素,有了 Sentinel 哨兵节点避免了代码中的判空逻辑 void link(Node<K, V> sentinel, Node<K, V> node) { node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder()); node.setNextInVariableOrder(sentinel); sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node); sentinel.setPreviousInVariableOrder(node); } } 其中 node.getVariableTime() 为元素的过期时间,那么这个过期时间是何时计算的呢?在 put 方法中有如下逻辑: abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef implements LocalCache<K, V> { @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) { requireNonNull(key); requireNonNull(value); Node<K, V> node = null; long now = expirationTicker().read(); int newWeight = weigher.weigh(key, value); Object lookupKey = nodeFactory.newLookupKey(key); for (int attempts = 1; ; attempts++) { Node<K, V> prior = data.get(lookupKey); if (prior == null) { if (node == null) { node = nodeFactory.newNode(key, keyReferenceQueue(), value, valueReferenceQueue(), newWeight, now); // 计算过期时间并为 variableTime 赋值 setVariableTime(node, expireAfterCreate(key, value, expiry, now)); } } // ... } // ... } long expireAfterCreate(@Nullable K key, @Nullable V value, Expiry<? super K, ? super V> expiry, long now) { if (expiresVariable() && (key != null) && (value != null)) { // 此处的 expireAfterCreate 便为自定义的有效期 long duration = expiry.expireAfterCreate(key, value, now); // 将定义的有效期在当前操作时间上进行累加得出过期时间 return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY)); } return 0L; } } 可以发现在元素被添加时它的过期时间已经被计算好了。接着回到 findBucket 方法,其中有逻辑 duration < SPANS[i + 1] 确定节点所在时间轮的层级。SPANS 数组中存储的是层级的“时间跨度边界”,如注释中标记的,SPANS[0] 表示第一层级的时间为 1.07s,SPANS[1] 表示第二层级的时间为 1.14m,for 循环开始时便以 SPANS[1] 作为比较,如果 duration 小于 SPANS[1],那么将该节点放在第一层级,那么第一层级的时间范围为 t < 1.14 min,为秒级的时间跨度;第二层级的时间范围为 1.14 min <= t < 1.22 h,为分钟级的时间跨度,接下来的时间跨度以此类推。在这里也明白了为什么 final Node<K, V>[][] wheel 数据结构会将第一行、第二行元素大小设定为 64,因为第一行为秒级别的时间跨度,60s 即为 1min,那么在这个秒级别的跨度下 64 的容量足以,同理,第二行为分钟级别的时间跨度,60min 即为 1h,那么在这个分钟级别的跨度下 64 的容量也足够了。 现在我们已经了解了向 TimeWheel 中添加元素的逻辑,那么现在我们可以回到文章开头提到的 maintenance 方法中调用的 TimeWheel#advance 方法了。 advance advance 有推进、前进的意思,我认为在 TimeWheel 中,advance 用来表示“某个层级的时间有没有流动”会更合适,以下是它的源码: final class TimerWheel<K, V> implements Iterable<Node<K, V>> { static final long[] SHIFT = { Long.numberOfTrailingZeros(SPANS[0]), Long.numberOfTrailingZeros(SPANS[1]), Long.numberOfTrailingZeros(SPANS[2]), Long.numberOfTrailingZeros(SPANS[3]), Long.numberOfTrailingZeros(SPANS[4]), }; long nanos; public void advance(BoundedLocalCache<K, V> cache, long currentTimeNanos) { // nanos 总是记录 advance 方法被调用时的时间 long previousTimeNanos = nanos; nanos = currentTimeNanos; // 校正时间戳的溢出 if ((previousTimeNanos < 0) && (currentTimeNanos > 0)) { previousTimeNanos += Long.MAX_VALUE; currentTimeNanos += Long.MAX_VALUE; } try { // 遍历所有层级,如果当前层级的时间没有流动,则结束循环 // 因为所有的层级是按照时间递增的顺序排列的,如果低层级的时间都没有流动,那么证明更高的层级时间更没有流动了,比如秒级别时间没变,那么分钟级别的时间更不可能变 for (int i = 0; i < SHIFT.length; i++) { long previousTicks = (previousTimeNanos >>> SHIFT[i]); long currentTicks = (currentTimeNanos >>> SHIFT[i]); // 计算出某层级下时间的流动范围 long delta = (currentTicks - previousTicks); if (delta <= 0L) { break; } // 如果当前层级的时间有流动,则调用 expire 方法 expire(cache, i, previousTicks, delta); } } catch (Throwable t) { nanos = previousTimeNanos; throw t; } } } 我们接着看 expire 方法: final class TimerWheel<K, V> implements Iterable<Node<K, V>> { final Node<K, V>[][] wheel; long nanos; void expire(BoundedLocalCache<K, V> cache, int index, long previousTicks, long delta) { Node<K, V>[] timerWheel = wheel[index]; int mask = timerWheel.length - 1; // 计算要遍历处理的槽位数量,假设 delta 不会出现负值(只有时间范围超过 2^61 nanoseconds (73 years) 才会溢出) int steps = Math.min(1 + (int) delta, timerWheel.length); // 上一次 advance 的操作时间即为起始索引 int start = (int) (previousTicks & mask); // 计算结果索引 int end = start + steps; for (int i = start; i < end; i++) { // 拿到桶中的哨兵节点,获取到尾节点和头节点 Node<K, V> sentinel = timerWheel[i & mask]; Node<K, V> prev = sentinel.getPreviousInVariableOrder(); Node<K, V> node = sentinel.getNextInVariableOrder(); // 重置哨兵节点,意味着这个桶中的元素都需要被处理,该层级的时间有流逝,处理但不意味着有元素过期 sentinel.setPreviousInVariableOrder(sentinel); sentinel.setNextInVariableOrder(sentinel); // node != sentinel 表示 node 并不是哨兵节点,证明其中有元素需要被处理 while (node != sentinel) { // 标记 next 节点的引用 Node<K, V> next = node.getNextInVariableOrder(); // 将当前节点从双向链表中断开 node.setPreviousInVariableOrder(null); node.setNextInVariableOrder(null); try { // 先拿元素的过期时间与当前操作时间比较,判断有没有过期,如果过期则会执行 evictEntry 方法,驱逐元素 if (((node.getVariableTime() - nanos) > 0) || !cache.evictEntry(node, RemovalCause.EXPIRED, nanos)) { // 如果没有过期则重新 schedule 节点,因为随着时间的流逝,该节点可能会被重新分配到更低的时间层级中,以便被更好的管理过期时间 schedule(node); } node = next; } catch (Throwable t) { // 处理时发生异常,将节点重新加入到链表中 node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder()); node.setNextInVariableOrder(next); sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node); sentinel.setPreviousInVariableOrder(prev); throw t; } } } } } expire 方法并不复杂,本质上是将未过期的节点重新执行 TimeWheel#schedule 方法,将其划分到更精准的时间分层;将过期的节点驱逐,evictEntry 方法在 缓存之美:万文详解 Caffeine 实现原理 中已经介绍过了,这里就不再赘述了。 总结一下 TimeWheel 的流程:只有指定了 expireAfter 时间过期策略的缓存才会使用到时间轮。当元素被添加时,它的过期时间已经被计算好并赋值到 variableTime 字段中,根据当前元素的剩余有效期(variableTime - nanos)划分它在具体的时间轮层级(wheel),随着时间的流逝(advance),它所在的时间轮层级可能会不断变化,可能由小时级别被转移(schedule)到分钟级,当然它也可能过期直接被驱逐(evictEntry)。这样操作按照元素剩余有效期将其划分到更精准的时间层级中,可以更精准的控制元素的过期时间,比如秒级时间没有流逝的话,那么便无需检查分钟级或更高时间跨度级别的元素是否过期。Caffeine 缓存完整的原理图如下: 详细了解需要结合 缓存之美:万文详解 Caffeine 实现原理。

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册