Caffeine Cache解析(三):BoundedBuffer 与 MpscGrowableArrayQueue 源码浅析
接续 Caffeine Cache解析(一):接口设计与TinyLFU
接续 Caffeine Cache解析(二):drainStatus多线程状态流转
BoundedBuffer 与 MpscGrowableArrayQueue
multiple-producer / single-consumer
这里multiple和single指的是并发数量
- BoundedBuffer: Caffeine中readBuffer字段实现 , A circular ring buffer
- MpscGrowableArrayQueue: Caffeine中writeBuffer字段实现, An MPSC array queue which starts at initialCapacity and grows to maxCapacity in linked chunks of the initial size. The queue grows only when the current buffer is full and elements are not copied on resize, instead a link to the new buffer is stored in the old buffer for the consumer to follow.
BoundedBuffer
BoundedBuffer继承自StripedBuffer,其中stripe表示条、带的意思,StripedBuffer即做了分段处理,
增加写入并发度。StripedBuffer类似一个门面,其中的volatile Buffer<E> @Nullable[] table;
实际存放数据,而这里的Buffer接口实现为BoundedBuffer的内部类RingBuffer。
StripedBuffer实现模仿jdk中的java.util.concurrent.atomic.Striped64
,比如java.util.concurrent.atomic.LongAdder extends Striped64
的并发累加器,其实现中有多个long的累加器,可以并发累加,最后获取sum的时候将所有累加器sum起来即可。除了支持并发写入,StripedBuffer还避免了扩容时的复制操作。
StripedBuffer的uml和原理示意图如下图:
刨去预防false sharing 的 pad相关代码,RingBuffer也是通过双指针来对一个固定size的数组进行循环利用,简化格式如下:
static final class RingBuffer<E> extends ... implements Buffer<E> {static final VarHandle BUFFER = MethodHandles.arrayElementVarHandle(Object[].class);static final VarHandle READ WRITE;static {...READ = lookup.findVarHandle(BBHeader.ReadCounterRef.class, "readCounter", long.class);WRITE = lookup.findVarHandle(BBHeader.ReadAndWriteCounterRef.class, "writeCounter", long.class);...}// 一个RingBuffer固定16个元素static final int BUFFER_SIZE = 16;// mask,取RingBuffer数组index用static final int MASK = BUFFER_SIZE - 1;// 不直接使用,通过BUFFER字段调用final Object[] buffer;// 类似于写入指针,但这里通过counter表示,单调递增,且一定大于等于readCounter// 不直接使用,通过WRITE字段调用volatile long writeCounter;// 类似于读取指针,但这里通过counter表示,单调递增,且一定小于等于writeCounter// 不直接使用,通过READ字段调用volatile long readCounter;public RingBuffer(E e) {buffer = new Object[BUFFER_SIZE];// 写buffer[]的第0个indexBUFFER.set(buffer, 0, e);// write计数器+1WRITE.set(this, 1);}@Overridepublic int offer(E e) {long head = readCounter;long tail = writeCounterOpaque();long size = (tail - head);// 环形数组,读写counter>=16即满了if (size >= BUFFER_SIZE) {return Buffer.FULL;}// 没满先cas writeCounter,失败直接返回failif (casWriteCounter(tail, tail + 1)) {// 计算index,// 极端举例,tail = 18, head最小只能是3, // 18 = 10010 和 1111做与 = 0010// 此时会写入到index = 2的位置,因为head=3之前的元素已经被处理过了,可以覆盖,实现循环利用int index = (int) (tail & MASK);BUFFER.setRelease(buffer, index, e);return Buffer.SUCCESS;}return Buffer.FAILED;}@Override@SuppressWarnings("Varifier")public void drainTo(Consumer<E> consumer) {@Var long head = readCounter;long tail = writeCounterOpaque();long size = (tail - head);// 特判if (size == 0) {return;}do {// 读取下一个元素int index = (int) (head & MASK);var e = (E) BUFFER.getAcquire(buffer, index);if (e == null) { // write还没写进去,因为写入是先cas writeCounter再写入元素到数组,所以存在为null的情况// not published yetbreak;}BUFFER.setRelease(buffer, index, null);consumer.accept(e);head++;} while (head != tail);// readerCounter归位setReadCounterOpaque(head);}...}
}
那么StripedBuffer面对并发offer时,如何实现的呢?
abstract class StripedBuffer<E> implements Buffer<E> {@Overridepublic int offer(E e) {// 计算一个hash值h,用于定位此次写入Buffer<E>[]的indexlong z = mix64(Thread.currentThread().getId());int increment = ((int) (z >>> 32)) | 1;int h = (int) z;int mask; // mask,用于根据hash值计算出对应的indexint result; // 结果,0成功 -1失败,1满了BoundedBuffer.RingBuffer<E> buffer;boolean uncontended = true; // 是否存在竞争,即RingBuffer的cas是否成功Buffer<E>[] buffers = table;if ((buffers == null) // 第一次buffers还没初始化,直接走到expandOrRetry|| ((mask = buffers.length - 1) < 0) // 即buffers.length == 0,buffers还没初始化完成|| ((buffer = (BoundedBuffer.RingBuffer<E>) buffers[h & mask]) == null) // 定位到的buffers[]的RingBuffer// 实际执行插入到RingBuffer中,FAILED则expandOrRetry,如果FULL,则直接返回FULL,外层调用会主动调用drainTo|| !(uncontended = ((result = buffer.offer(e)) != Buffer.FAILED))) {// “扩容”或者重试return expandOrRetry(e, h, increment, uncontended);}return result;}/*** resize和create Buffers时cas的标志位*/volatile int tableBusy;// 包含初始化、扩容、创建Bufferfinal int expandOrRetry(E e, int h, int increment, boolean wasUncontended) {int result = Buffer.FAILED;boolean collide = false; // True if last slot nonemptyfor (int attempt = 0; attempt < ATTEMPTS; attempt++) { // 重试3次, 3次是有含义的,往下看Buffer<E>[] buffers;Buffer<E> buffer;int n;if (((buffers = table) != null) && ((n = buffers.length) > 0)) { // buffers初始化了if ((buffer = buffers[(n - 1) & h]) == null) { // 定位RingBuffer如果为nullif ((tableBusy == 0) && casTableBusy()) { // tableBusy标志位设置成功boolean created = false;try { // Recheck under lockBuffer<E>[] rs;int mask;int j;// 再次判断RingBuffer是否为空if (((rs = table) != null) && ((mask = rs.length) > 0)&& (rs[j = (mask - 1) & h] == null)) {// 创建一个RingBuffer,调用BoundedBuffer#creaters[j] = create(e);created = true;}} finally {tableBusy = 0;}if (created) {result = Buffer.SUCCESS;break;}continue; // Slot is now non-empty}collide = false;} else if (!wasUncontended) {// 走到这里只有在StripedBuffer的offer方法的if里的最后buffer#offer失败的情况wasUncontended = true;} else if ((result = buffer.offer(e)) != Buffer.FAILED) { // 【再次重试】// RingBuffer插入SUCCESS或FULL则直接返回break;} else if ((n >= MAXIMUM_TABLE_SIZE) || (table != buffers)) {// buffers已经到最大值或table已经不是最初的buffers(即经过Arrays.copyOf扩容)collide = false; // At max size or stale} else if (!collide) { // 上面【再次重试】还是失败,collide碰撞标记设为truecollide = true;} else if ((tableBusy == 0) && casTableBusy()) {// 走到这里是,先【再次重试】失败,然后到上面的if将collide设为true// 然后下一次循环【再次重试】失败和后面的if判断失败就会走这里实施扩容try {if (table == buffers) {// 扩容table,2倍table = Arrays.copyOf(buffers, n << 1);}} finally {tableBusy = 0;}collide = false;// 扩容后不会重试offer,而是在下一次for循环offer插入,所以重试的ATTEMPTS==3continue; // Retry with expanded table}// hash值变化增加一下h += increment;}// 执行初始化// 如果tableBusy标志位不是1,且cas设置为1成功else if ((tableBusy == 0) && (table == buffers) && casTableBusy()) {boolean init = false;try { // Initialize tableif (table == buffers) {Buffer<E>[] rs = new Buffer[1];// create实际调用BoundedBuffer#create方法返回RingBufferrs[0] = create(e);table = rs;init = true;}} finally {tableBusy = 0;}if (init) {result = Buffer.SUCCESS;break;}}}return result;}...
}
MpscGrowableArrayQueue
MpscGrowableArrayQueue优势是在扩容时不需要copy数组,只需要重新分配数组并使用指针链接
先来看下几个名词:
- producerBuffer: 写入元素数组
- consumerBuffer: 读取元素数组,当读取及时时可以和producerBuffer是同一个数组
- producerIndex(pIndex): 写入元素的数量 * 2,为什么*2 ?因为奇数表示扩容中,代码中很多数值都乘以2了,要注意辨别
- consumerIndex(cIndex): 读取元素的数量 * 2
- offset: buffer[]中的元素的index,可由 pIndex 和 cIndex 和mask 转化而来
(pIndex & mask) >> 1
- mask: 掩码,用于计算offset和capacity(实际大小,非乘以2的值),为n位1+末位0,如 6=110,14=1110,30=11110
- JUMP: 静态标识变量,放入buffer[]中,表示需要到nextBuffer的相同index找元素
- maxQueueCapacity:数组最大容量 * 2
MpscGrowableArrayQueue的数据结构和扩容方式如下图:
![]() | ![]() |
---|
看下计算规则:
// initialCapacity为构造函数传入// initialCap -> p2capacity: 6 -> 8, 7 -> 8, 8 -> 8, 9 -> 16int p2capacity = ceilingPowerOfTwo(initialCapacity);// 末位为0的掩码// p2capacity -> mask(二进制表示) : 4 -> 110, 8 -> 1110, 16 -> 11110long mask = (p2capacity - 1L) << 1;// +1的是存放指向nextBuffer[]的指针E[] buffer = allocate(p2capacity + 1);// 扩容buffer[]的长度根据现有buffer[]的长度计算得来int newSize = 2 * (buffer.length - 1) + 1// 注意,buffer[]的length和capacity表示不同的东西// length表示数组的长度// capacity表示数组可存放元素的数量(不含JUMP 和nextBuffer指针)protected long getCurrentBufferCapacity(long mask) {// 根据构造函数中规则可知// mask = (p2capacity - 1L) << 1 = 2 * p2capacity - 2; 这里p2Capacity真实没乘2的容量,是initialCapacity向上取最小的2的n次方// curBufferLength = p2capacity + 1// 又根据扩容规则 nextBufferLength = 2 * (curBufferLength - 1) + 1 = 2 * p2capacity + 1 = (mask + 2) + 1// 所以每次扩容都是把 p2capacity * 2,然后再加一个指针的1// 但是如果 p2capacity * 2 已经达到了 maxQueueCapacity,也就不需要预留向后扩容用的指针了// 直接把原来存放指针的地方用来存放元素,扩大一个容量return (mask + 2 == maxQueueCapacity) ? maxQueueCapacity : mask;}
class MpscGrowableArrayQueue<E> extends ...BaseMpscLinkedArrayQueue... {// 应理解为当前prodce过的元素的 count * 2,可以转化为producerBuffer的索引,通过VarHandle操作protected long producerIndex; protected long producerMask;// 写入bufferprotected E[] producerBuffer;protected volatile long producerLimit;// 应理解为当前consume过的元素的 count * 2,可以转化为consumerBuffer的索引protected long consumerIndex; protected long consumerMask;// 读取buffer,当消费及时时可以和producerBuffer是同一个数组protected E[] consumerBuffer; // 表示获取nextBuffer的相同index的元素private static final Object JUMP = new Object();protected final long maxQueueCapacity;...
}
abstract class BaseMpscLinkedArrayQueue<E> ... {...BaseMpscLinkedArrayQueue(int initialCapacity) {if (initialCapacity < 2) {throw new IllegalArgumentException("Initial capacity must be 2 or more");}// 6 -> 8, 7 -> 8, 8 -> 8, 9 -> 16int p2capacity = ceilingPowerOfTwo(initialCapacity);// leave lower bit of mask clear// 8 = 1000 -> 1000 - 1 = 0111 -> 0111 << 1 = 1110 = 14// 0000 0000 0000 00long mask = (p2capacity - 1L) << 1;// need extra element to point at next arrayE[] buffer = allocate(p2capacity + 1);producerBuffer = buffer;producerMask = mask;consumerBuffer = buffer;consumerMask = mask;soProducerLimit(this, mask); // we know it's all empty to start with}@Overridepublic boolean offer(E e) {if (e == null) {throw new NullPointerException();}long mask; // mask 即 当前buffer的sizeE[] buffer;// pIndex表示两个含义:// 1. 使用最后一位表示是否在扩容,最后一位为1(奇数)表示在扩容// 2. 使用pIndex >> 2 即 pIndex/2 表示最新写入元素在buffer数组中的索引,后面代码称为offsetlong pIndex;while (true) {// lv表示 volatile load (load + LoadLoad barrier)long producerLimit = lvProducerLimit();pIndex = lvProducerIndex(this);// lower bit is indicative of resize, if we see it we spin until it's cleared// 奇数表示正在扩容,自旋if ((pIndex & 1) == 1) {continue;}// mask/buffer may get changed by resizing -> only use for array access after successful CAS.mask = this.producerMask;buffer = this.producerBuffer;// a successful CAS ties the ordering, lv(pIndex)-[mask/buffer]->cas(pIndex)// 这里快速判断是否需要扩容,不需要再直接写入元素到现有buffer[]if (producerLimit <= pIndex) {// 内层int result = offerSlowPath(mask, pIndex, producerLimit);switch (result) {case 0:break;case 1:continue;case 2:return false;case 3:resize(mask, buffer, pIndex, e);return true;}}// 先cas更新pIndexif (casProducerIndex(this, pIndex, pIndex + 2)) {break;}}// cas更新pIndex成功后再设置元素,这里的offset是才是数组的索引long offset = modifiedCalcElementOffset(pIndex, mask);// so表示set offsetsoElement(buffer, offset, e);return true;}/*** We do not inline resize into this method because we do not resize on fill.*/// 此时pIndex <= producerLimitprivate int offerSlowPath(long mask, long pIndex, long producerLimit) {int result;long cIndex = lvConsumerIndex(this);long bufferCapacity = getCurrentBufferCapacity(mask);result = 0; // 0 - goto pIndex CASif (cIndex + bufferCapacity > pIndex) {if (!casProducerLimit(this, producerLimit, cIndex + bufferCapacity)) {// 1表示重试result = 1;}}// pIndex和cIndex相差超过maxQueueCapacity了,即满了// 注意这里maxQueueCapacity是2倍的bufferCapacity,即maxQueueCapacity = 2 * bufferCapacity,和pIndex逻辑一样else if (availableInQueue(pIndex, cIndex) <= 0) {// 2表示失败result = 2;}// pIndex设置为奇数,表示正在扩容else if (casProducerIndex(this, pIndex, pIndex + 1)) {// 扩容result = 3;} else {// 扩容失败,重试result = 1;}return result;}@Overrideprotected long getCurrentBufferCapacity(long mask) {// 根据构造函数中规则可知// mask = (p2capacity - 1L) << 1 = 2 * p2capacity - 2; 这里p2Capacity真实没乘2的容量,是initialCapacity向上取最小的2的n次方// curBufferLength = p2capacity + 1// 又根据扩容规则 nextBufferLength = 2 * (curBufferLength - 1) + 1 = 2 * p2capacity + 1 = (mask + 2) + 1// 所以每次扩容都是把 p2capacity * 2,然后再加一个指针的1// 但是如果 p2capacity * 2 已经达到了 maxQueueCapacity,也就不需要预留向后扩容用的指针了// 直接把原来存放指针的地方用来存放元素,扩大一个容量return (mask + 2 == maxQueueCapacity) ? maxQueueCapacity : mask;}protected long availableInQueue(long pIndex, long cIndex) {return maxQueueCapacity - (pIndex - cIndex);}/*** poll只能单线程处理*/@Override@SuppressWarnings({"CastCanBeRemovedNarrowingVariableType", "unchecked"})public E poll() {E[] buffer = consumerBuffer;long index = consumerIndex;long mask = consumerMask;long offset = modifiedCalcElementOffset(index, mask);Object e = lvElement(buffer, offset);// LoadLoadif (e == null) {if (index != lvProducerIndex(this)) {// e当且仅当queue是空的// 但仅仅通过 e==null 不能表示queue为空,得看producerIndex和consumerIndex的关系// consumerIndex != producerIndex 且 e == null 说明有producer正在插入(插入是先cas pIndex再插入元素)// 自旋do {e = lvElement(buffer, offset);} while (e == null);} else {// 此时consumerIndex == producerIndex,说明都poll了,队列为空return null;}}if (e == JUMP) {// JUMP到链接的下一个bufferE[] nextBuffer = getNextBuffer(buffer, mask);// 取array中相同的index的元素,并更新元素和consumerIndexreturn newBufferPoll(nextBuffer, index);}// 更新元素soElement(buffer, offset, null);// 更新consumerIndexsoConsumerIndex(this, index + 2);return (E) e;}/*** This method assumes index is actually (index << 1) because lower bit is used for resize. This* is compensated for by reducing the element shift. The computation is constant folded, so* there's no cost.*/// index = 2 -> offset = 1, index = 4 -> offset = 2static long modifiedCalcElementOffset(long index, long mask) {return (index & mask) >> 1;}private void resize(long oldMask, E[] oldBuffer, long pIndex, E e) {// 扩容规则int newBufferLength = getNextBufferSize(oldBuffer);E[] newBuffer = allocate(newBufferLength);producerBuffer = newBuffer;int newMask = (newBufferLength - 2) << 1;producerMask = newMask;// 计算buffer[]中的index,这里叫offsetlong offsetInOld = modifiedCalcElementOffset(pIndex, oldMask);long offsetInNew = modifiedCalcElementOffset(pIndex, newMask);// set 元素soElement(newBuffer, offsetInNew, e);// element in new array// set newBuffer[]指针soElement(oldBuffer, nextArrayOffset(oldMask), newBuffer);// buffer linked...// Invalidate racing CASs// We never set the limit beyond the bounds of a buffersoProducerLimit(this, pIndex + Math.min(newMask, availableInQueue));// make resize visible to the other producerssoProducerIndex(this, pIndex + 2);// INDEX visible before ELEMENT, consistent with consumer expectation// make resize visible to consumersoElement(oldBuffer, offsetInOld, JUMP);}// 扩容规则@Overrideprotected int getNextBufferSize(E[] buffer) {long maxSize = maxQueueCapacity / 2;// maxQueueCapacity是实际capacity的2倍,所以这里buffer.length 肯定不大于 maxSizeif (buffer.length > maxSize) {throw new IllegalStateException();}int newSize = 2 * (buffer.length - 1);return newSize + 1;}}
相关文章:

Caffeine Cache解析(三):BoundedBuffer 与 MpscGrowableArrayQueue 源码浅析
接续 Caffeine Cache解析(一):接口设计与TinyLFU 接续 Caffeine Cache解析(二):drainStatus多线程状态流转 BoundedBuffer 与 MpscGrowableArrayQueue multiple-producer / single-consumer 这里multiple和single指的是并发数量 BoundedBuffer: Caf…...

全双工通信协议WebSocket——使用WebSocket实现智能学习助手/聊天室功能
一.什么是WebSocket? WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器的全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输 HTTP 协议是一种无状态的、无连接的、单向的应用…...

Rust-Trait 特征编程
昨夜江边春水生,艨艟巨舰一毛轻。 向来枉费推移力,此日中流自在行。 ——《活水亭观书有感二首其二》宋朱熹 【哲理】往日舟大水浅,众人使劲推船,也是白费力气,而此时春水猛涨,巨舰却自由自在地飘行在水流中…...

彻底理解哈希表(HashTable)结构
目录 介绍优缺点概念哈希函数快速的计算键类型键转索引霍纳法则 均匀的分布 哈希冲突链地址法开放地址法线性探测二次探测再哈希法 扩容/缩容实现哈希创建哈希表质数判断哈希函数插入&修改获取数据删除数据扩容/缩容函数全部代码 哈希表(Hash Table)…...

微信小程序的汽车维修预约管理系统
文章目录 项目介绍具体实现截图技术介绍mvc设计模式小程序框架以及目录结构介绍错误处理和异常处理java类核心代码部分展示详细视频演示源码获取 项目介绍 系统功能简述 前台用于实现用户在页面上的各种操作,同时在个人中心显示各种操作所产生的记录:后…...
LeetCode:3255. 长度为 K 的子数组的能量值 II(模拟 Java)
目录 3255. 长度为 K 的子数组的能量值 II 题目描述: 实现代码与解析: 模拟 原理思路: 3255. 长度为 K 的子数组的能量值 II 题目描述: 给你一个长度为 n 的整数数组 nums 和一个正整数 k 。 一个数组的 能量值 定义为&am…...

深入了解逻辑回归:机器学习中的经典算法
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
软件测试基础十三(python 函数)
函数 1. 函数的意义 代码复用 提高效率:Python中的函数允许将一段可重复使用的代码封装起来。例如,在一个数据分析项目中,可能需要多次计算一组数据的平均值。可以将计算平均值的代码定义为一个函数: def calculate_average(nu…...

计算机网络——HTTP篇
基础篇 IOS七层网络模型 TCP/IP四层模型? 应⽤层:位于传输层之上,主要提供两个终端设备上的应⽤程序之间的通信,它定义了信息交换的格式,消息会交给下⼀层传输层来传输。 传输层的主要任务就是负责向两台设备进程之间…...

信息化运维方案,实施方案,开发方案,信息中心安全运维资料(软件资料word)
1 编制目的 2 系统运行维护 2.1 系统运维内容 2.2 日常运行维护方案 2.2.1 日常巡检 2.2.2 状态监控 2.2.3 系统优化 2.2.4 软件系统问题处理及升级 2.2.5 系统数据库管理维护 2.2.6 灾难恢复 2.3 应急运行维护方案 2.3.1 启动应急流程 2.3.2 成立应急小组 2.3.3 应急处理过程 …...

自动化工具 Gulp
自动化工具 gulp 摘要 概念:gulp用于自动化开发流程。 理解:我们只需要编写任务,然后gulp帮我们执行 核心概念: 任务:通过定义不同的任务来组织你的构建流程。 管道:通过管道方式将文件从一个插件传递…...
css实现div被图片撑开
固定好盒子的宽度,高度随传过来的图片大小决定 <div class"tab-con"> <img:src"concertInfo.detail"alt""> </div>.tab-con {margin-bottom: 20px;width: 700px;img {width: 700px;height: auto;object-fit: cont…...
Power Pivot、Power BI 和 SQL Server Analysis Services 的公式语言:DAX(数据分析表达式)
DAX(Data Analysis Expressions)是一种用于 Power Pivot、Power BI 和 SQL Server Analysis Services 的公式语言,旨在帮助用户进行数据建模和复杂计算。DAX 的设计初衷是使数据分析变得简单而高效,特别是在处理数据模型中的表关系…...

大模型应用编排工具Dify二开之工具和模型页面改造
1.前言 简要介绍下 dify: 一款可以对接市面上主流大模型的任务编排工具,可以通过拖拽形式进行编排形成解决某些业务场景的大模型应用。 背景信息: 环境:dify-0.8.3、docker-21 最近笔者在做 dify的私有化部署和二次…...

Pytorch用BERT对CoLA、新闻组文本数据集自然语言处理NLP:主题分类建模微调可视化分析...
原文链接:https://tecdat.cn/?p38181 自然语言处理(NLP)领域在近年来发展迅猛,尤其是预训练模型的出现带来了重大变革。其中,BERT 模型凭借其卓越性能备受瞩目。然而,对于许多研究者而言,如何高…...
LightGBM-GPU不能装在WSL,能装在windows上
这是一篇经验总结文章,注重思路,忽略细节。 1.起因 用多个机器学习方法训练模型,比较性能,发现Light GBM方法获得的性能明显更高,但问题是在CPU上训练的速度特别特别慢,需要用GPU训练。 2.开始装LightGB…...

工业相机常用功能之白平衡及C++代码分享
目录 1、白平衡的概念解析 2、相机白平衡参数及操作 2.1 相机白平衡参数 2.2 自动白平衡操作 2.3 手动白平衡操作流程 3、C++ 代码从XML读取参数及设置相机参数 3.1 读取XML 3.2 C++代码,从XML读取参数 3.3 给相机设置参数 1、白平衡的概念解析 白平衡(White Balance)…...
Foundry 单元测试
安装 Foundry 如果你还没有安装 Foundry,请按照此处的说明进行操作:Foundry 安装 Foundry Hello World 只需运行以下命令,它将为你设置环境,创建测试并运行它们。(当然,这假设你已经安装了 Foundry&…...

idea database连接数据库后看不到表解决方法、格式化sql快捷键
最下面那个勾选上就可以了 或 格式化sql快捷键: 先选中, 使用快捷键格式化 SQL: Windows/Linux: Ctrl Alt L macOS: Cmd Alt L...
【数学二】线性代数-向量-向量组的秩、矩阵得秩
考试要求 1、理解 n n n维向量、向量的线性组合与线性表示的概念. 2、理解向量组线性相关、线性无关的概念,掌握向量组线性相关、线性无关的有关性质及判别法. 3、了解向量组的极大线性无关组和向量组的秩的概念,会求向量组的极大线性无关组及秩. 4、了解向量组等价的概念,…...

接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...

19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
Java编程之桥接模式
定义 桥接模式(Bridge Pattern)属于结构型设计模式,它的核心意图是将抽象部分与实现部分分离,使它们可以独立地变化。这种模式通过组合关系来替代继承关系,从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...