Go 本地缓存 bigcache

news2025/1/9 16:53:11

​本地缓存已经算是数据库的常规设计了,因为数据直接缓存在机器内存中,避免昂贵的IO开销,数据的读取和写入快到不可思议。本地缓存常驻在内存中,就好像业务逻辑中声明的全局变量一样,不会被垃圾回收。但本地内存也会导致单机内存使用率变高,频繁的GC导致系统性能下降。

在说 Go 本地内存之前,我们先看看别的数据库是怎么使用本地内存的。我查了一下,觉得 HBase 本地内存和 Go 的设计特别的接近,我们来简单看看 HBase 的 BlockCache:

BlockCache

HBase 实现的缓存结构称为 BlockCache,用来缓存 Block 对象,它实现了3种 BlockCache 的缓存方案,我们来最早的设计实现 LRUBlockCache,看名字就知道缓存淘汰策略是 LRU,该策略将所有数据都放入到 JVM Heap 中,交给 JVM 进行管理。而 JVM 的垃圾回收机制注定这个策略存在缺陷,FullGC对业务的读写请求会有很大的影响。后来,引入了后续的堆外内存分配。

多级缓存

在缓存设计中,常用的手段就是设计多级缓存,可能大家都参考了CPU读取缓存的设计。既然是多级缓存,每一级缓存之间总得存在些差异,基于缓存大小、数据热度、过期时间这些维度,就可以做到层级缓存之间的区分。​​​​​​​​​​​​​​​​​​
在这里插入图片描述
上图简单的示意一个多级缓存,包含了三个层级,每个层级的内存容量都不相同,level3的容量是最小的,相对应它的存储介质可以选择最好的(当然三个层级存储介质也可以相同),有点类似数据先查询本地内存数据,查询不到查询 Redis 数据,再继续查询 MySQL 的意思。

假设这三级缓存的存储介质都是相同的,我们还可以从数据的冷热层度进行区分,最初数据都存储在level1中,如果数据被多次访问,就可以迁移到 level2 中,再超过一定的阈值,就可以迁移到 level3中。但要明确区分这三个级别,还需要引入另一个参考因素,比如缓存时间,更热的数据值得缓存更长的时间。

但这也面临一个问题,在数据不存在的情况下,数据查询也会被增加到3次。究竟是否要使用多级缓存,还是要结合实际业务去看,毕竟,多级缓存带来的也有代码复杂度的提升。

HBase采用了缓存分层设计,它包含三个部分:single-access、multi-access、in-memory,分别占到整个BlockCache大小的25%、50%、25%。在一次随机读中,一个Block从HDFS加载出来之后首先放入 single-access 区,如果后续有多次请求访问这个 Block,就会将这个 Block 移动到 mutlti-access 区。而 in-memory 区表示数据可以常驻内存,一般用来存放访问频繁、量小的数据。

缓存淘汰策略

在选择本地缓存时,缓存淘汰策略是非常重要的考量因素,进程的内存容量是有限的,当缓存容量超过一定的限度时,缓存中的哪些数据应该被淘汰呢?最完美的状态是移除那些不再被使用的数据,这需要我们基于业务形态最决策,选择最适合的淘汰策略。

LRU

常见的 LRU 缓存,基于最近最少使用原则对缓存数据进行淘汰,它基于的假设是:最近被访问的数据大概率后续还会被访问。如果这个数据是热点数据,LRU的性能应该挺好的。

FIFO

我在阅读 bigcache 的时候,作者采用的 FIFO 的缓存策略。 FIFO顾名思义,先进先出,当缓存满了之后,最早缓存的数据会被淘汰,缓存的底层数据结构支持是队列。这种策略实现起来会比较简单,正常来模拟一个循环队列,不断进行数据覆写就可以。这种方式对所有数据一视同仁,不考虑数据本身是否为热点数据。
在这里插入图片描述

谈一下我的想法,使用FIFO的话要控制好缓存的容量,淘汰的缓存和新增的缓存能有一个相对稳定的动态平衡。当缓存容量达到上限时,每新加入一个缓存,势必要淘汰一个缓存,如果有一个批量的加缓存操作,缓存就会频繁被淘汰出去,缓存的效果也就大打折扣。

假设缓存最多能容纳 N 个数据,如果不考虑缓存的业务过期时间,缓存的生命周期就是从加入缓存开始,到之后再加入 N 个新的数据为止。如果将FIFO的队列假设为一个环形队列,绿色表示新加入的缓存,如果重新加入新的缓存空,红色的缓存数据就会被覆盖。

在这里插入图片描述
缓存的淘汰就由两个因素决定,首先是缓存数据的有效期,假设数据的过期时间是 5m,如果新加入的绿色数据已经持续了 5m,那么绿色数据之前加入的所有数据都应该被清空。另一个因数就是缓存的容量了。

总结来说,要想让 FIFO 发挥大的作用,就是要让缓存队列不满,要么缓存的内存总容量要超过实际的缓存容量,要么限制缓存的对象数量。

图中我使用了循环队列来解释FIFO,主要是因为后续要介绍的 bigcache 使用的也是循环队列, 后面也要介绍实现细节,所以没有采用链表说明。

BigCace

GitHub地址在:github.com/allegro/bigcache。Usage非常简单。

内存容量限制

本地缓存存储数据有两个维度,一个是容量,比如2G的内容容量限制;另一个是数据条数,比如最多缓存20000条数据。bigCache 采用的是容量限制,但提供了两个比较容易混淆的配置项 MaxEntrySize 和 HardMaxCacheSize,其中,HardMaxCacheSize 用来限制最大可使用的物理内存,单位是 MB。当缓存大小超过这个容量限制,需要执行 FIFO 淘汰策略。因为 bigCache 采用 sharding 的分片策略,所以,具体到每个 shard 的分片内存大小,还需要平均折算一下。

MaxEntrySize 用来指定每个缓存对象的最大大小,单位是 byte,这其实是一个预估值,如果实际缓存对象大于 MaxEntrySize,数据仍然可以写入到 bigCache 中。正如注释所言,这个配置只是用来预估初始内存容量大小的。它需要和 MaxEntriesInWindow 配合使用,用来计算初始化 shard 的内存大小。如果这两个值评估的比较合适,可以有效避免底层内存扩容带来的性能开销问题。

HardMaxCacheSize 用来给 shard 设置一个内存边界,MaxEntrySize 和 MaxEntriesInWindow 用来设置初始化 shard 的内存大小。举个例子,我们允许 bigCache 使用的最大内存为 800M,我们预计实际使用的内存只需要 400M,就可以通过这3个属性配合来实现。

我们参考下面的代码来理解,这是初始化 shard 的配置方法。其中,maximumShardSizeInBytes 就是通过 HardMaxCacheSize 计算出来的结果,而 bytesQueueInitialCapacity 是通过 MaxEntrySize 和 HardMaxCacheSize 计算出来的结果。可以看出,最终其内存决定作用的还是
maximumShardSizeInBytes。

func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheShard {
	bytesQueueInitialCapacity := config.initialShardSize() * config.MaxEntrySize
	maximumShardSizeInBytes := config.maximumShardSizeInBytes()
	if maximumShardSizeInBytes > 0 && bytesQueueInitialCapacity > maximumShardSizeInBytes {
		bytesQueueInitialCapacity = maximumShardSizeInBytes
	}
	return &cacheShard{
		hashmap:      make(map[uint64]uint32, config.initialShardSize()),
		hashmapStats: make(map[uint64]uint32, config.initialShardSize()),
		entries:      *queue.NewBytesQueue(bytesQueueInitialCapacity, maximumShardSizeInBytes, config.Verbose),
		entryBuffer:  make([]byte, config.MaxEntrySize+headersSizeInBytes),
		onRemove:     callback,

		isVerbose:    config.Verbose,
		logger:       newLogger(config.Logger),
		clock:        clock,
		lifeWindow:   uint64(config.LifeWindow.Seconds()),
		statsEnabled: config.StatsEnabled,
		cleanEnabled: config.CleanWindow > 0,
	}
}

内存间隙

bigCache 存在内存间隙的问题,基于 FIFO 的缓存策略,当我们对缓存中的对象进行更新时,缓存的对象首先需要被删除,然后再重新将新值 push 到队列中。但这个理应被删除的对象,实际并没有从队列中真实删除,只是被标记为了“无效”,内存占用还是存在的,而这些间隙其实浪费了新对象的空间。

参考 bigCache 写缓存的代码实现,这个过程非常模式化。如果在缓存中查找到,就直接删除。然后向缓存中插入新的值,如果此时缓存容量已经到达最大值,写不进去的话,就开始 FIFO 的淘汰策略(如果待设置的缓存和已存在的缓存数据相等,也仍然执行删除的流程)。

func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error {
	currentTimestamp := uint64(s.clock.Epoch())

	s.lock.Lock()

	if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 {
		if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil {
			resetKeyFromEntry(previousEntry)
			//remove hashkey
			delete(s.hashmap, hashedKey)
		}
	}

	if !s.cleanEnabled {
		if oldestEntry, err := s.entries.Peek(); err == nil {
			s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry)
		}
	}

	w := wrapEntry(currentTimestamp, hashedKey, key, entry, &s.entryBuffer)

	for {
		if index, err := s.entries.Push(w); err == nil {
			s.hashmap[hashedKey] = uint32(index)
			s.lock.Unlock()
			return nil
		}
		if s.removeOldestEntry(NoSpace) != nil {
			s.lock.Unlock()
			return fmt.Errorf("entry is bigger than max shard size")
		}
	}
}

bigCache 是如何标记这个对象已失效呢?这个失效和缓存到达过期时间的失效肯定是不相同的。当这块失效的内存前面的所有缓存数据都过期时,这块内存才可以重新被利用起来。

假设一个场景,一旦缓存对象写入 bigCache 后,就不再对这个对象进行更新,一直等到这个对象过期,然后重新写入 bigCache。这个场景就不会产生内存间隙,是一种比较理想的情况。

Omitting GC

当map中存储过百万的object时,Go语言自身的GC甚至会影响不相关的请求,即使是对一个空对象做Marsh操作,响应时间也可能在1s以上。所以,如何避免Go默认对map做的Garbage Collector至关重要。

GC回收heap中对象,所以我们不把对象创建在heap中就可以避过垃圾回收。查阅offheap。
使用freecache.
在map结构的key和value中不存储pointer,这样便可以将map创建在堆上,同时忽略GC的影响。这来源于Go的优化.

Concurrency

为了避免加锁成为系统的瓶颈,BigTable采用了Shared的方式来解决,确实也有点Redis单线程的感觉。将一块大的数据划分成多块小的数据,为小数据块加锁,确实很好的缓解了加锁的瓶颈。这体现出了拆分的思想,突然想到了曾经被面试的问题:“请将2G的数据进行排序”。

我比较好奇它的Hash方法,客户端的key转换为实际存储的hashedKey的过程。请看通过hashedKey获取shard的部分,作者没有使用%取余来实现,而是使用了&与运算来替代,确实很注重细节啊!

说到与运算:0&0=0; 0&1=0; 1&0=0; 1&1=1;,所以,最终拆分个数完全取决与二进制中1的数量。如果shardMask等于3,那就可以拆分成4份,如果等于4,那结果就是2份,以此类推。

//通过客户端的key获取实际存储的key
// Sum64 gets the string and returns its uint64 hash value.
func (f fnv64a) Sum64(key string) uint64 {
	var hash uint64 = offset64
	for i := 0; i < len(key); i++ {
		hash ^= uint64(key[i])
		hash *= prime64
	}

	return hash
}

//通过实际存储的key获取shard块,使用与运算。
func (c *BigCache) getShard(hashedKey uint64) (shard *cacheShard) {
	return c.shards[hashedKey&c.shardMask]
}

Entry中存储的数据

这也是我特别好奇的地方。因为作者只简单介绍了它是模拟queue实现的,而且在map的结构中,它存储的仅仅是offset。那么,它是如何通过一个offset来获取到完整的数据信息的?

如代码所示,每个entry由5部分组成,分别是时间戳(8byte)、key的hash值(8byte)、key的长度(2byte)、key的值本身以及value的值本身。这里通过小端字节序来存储,所以后续的反编译也应该指定这种模式。从PutUint64、PutUint16也可以对应到字节的大小。

func wrapEntry(timestamp uint64, hash uint64, key string, entry []byte, buffer *[]byte) []byte {
	keyLength := len(key)
	blobLength := len(entry) + headersSizeInBytes + keyLength

	if blobLength > len(*buffer) {
		*buffer = make([]byte, blobLength)
	}
	blob := *buffer

	binary.LittleEndian.PutUint64(blob, timestamp)
	binary.LittleEndian.PutUint64(blob[timestampSizeInBytes:], hash)
	binary.LittleEndian.PutUint16(blob[timestampSizeInBytes+hashSizeInBytes:], uint16(keyLength))
	copy(blob[headersSizeInBytes:], key)
	copy(blob[headersSizeInBytes+keyLength:], entry)

	return blob[:blobLength]
}

queue存储实现

我们通过源码来分析一下 bigcache 是如何使用数组来模拟 FIFO 的,queue 操作由结构体 BytesQueue 对象来实现,核心点在于 Push 和 Pop 两个方法,Push负责向队列中写入数据,Pop负责从队列中“淘汰”数据。

Push代码中调用函数的命名做到了“见名知意”,canInsertAfterTail 和 canInsertBeforeHead 分别用来判断新的数据是否可以在 tail 之后插入,如果不可以,是否可以在 head 之前插入。插入到 tail 之后比较符合我们一般的认知,但在 head 之前插入,就有点反认知了。如果可以在 head 之前插入,tail 就会被赋值给一个新的初始值,这个时候,tail < head。所谓的“循环”就开始了。

关于 leftMarginIndex 这个常量, BytesQueue 结构体的 Reset 方法也有使用到,就是 head 和 tail 的初始状态,数值为1。是什么原因让作者不从 0 开始的呢?

// Push copies entry at the end of queue and moves tail pointer. Allocates more space if needed.
// Returns index for pushed data or error if maximum size queue limit is reached.
func (q *BytesQueue) Push(data []byte) (int, error) {
	neededSize := getNeededSize(len(data))

	if !q.canInsertAfterTail(neededSize) {
		if q.canInsertBeforeHead(neededSize) {
			q.tail = leftMarginIndex
		} else if q.capacity+neededSize >= q.maxCapacity && q.maxCapacity > 0 {
			return -1, &queueError{"Full queue. Maximum size limit reached."}
		} else {
			q.allocateAdditionalMemory(neededSize)
		}
	}

	index := q.tail

	q.push(data, neededSize)

	return index, nil
}

上面有了 tail 循环,那个 head 什么时候会再次循环到 tail 的前面呢?就只能是 Pop 方法了,当 head 移动到队列的右边界的时候,head就会被重新初始化为 leftMarginIndex。源码中 rightMargin 是一个程序动态计算的结果,而 leftMarginIndex 是一个常量。

// Pop reads the oldest entry from queue and moves head pointer to the next one
func (q *BytesQueue) Pop() ([]byte, error) {
	data, blockSize, err := q.peek(q.head)
	if err != nil {
		return nil, err
	}

	q.head += blockSize
	q.count--

	if q.head == q.rightMargin {
		q.head = leftMarginIndex
		if q.tail == q.rightMargin {
			q.tail = leftMarginIndex
		}
		q.rightMargin = q.tail
	}

	q.full = false

	return data, nil
}

可以看出,bigcache 的循环队列其实是通过4个要素来确定的,head、tail、队列开始边界、队列结束边界来,当head或者tail移动到队列结束边界时,都会被赋值队列开始边界,我们可以看 BytesQueue的结构体声明来验证这一点。因为左边界是常量,并不需要在结构体中进行声明。

type BytesQueue struct {
	full         bool
	array        []byte
	capacity     int
	maxCapacity  int
	head         int
	tail         int
	count        int
	rightMargin  int
	headerBuffer []byte
	verbose      bool
}

在这里插入图片描述

queue中每个元素都由2部分组成,前4个byte是数据的长度,后面是数据的值本身。其中PutUint32变需要4byte。所以queue中每个元素最下的长度应该是4,而它的值部分只能是0了。

func (q *BytesQueue) push(data []byte, len int) {
	binary.LittleEndian.PutUint32(q.headerBuffer, uint32(len))
	q.copy(q.headerBuffer, headerEntrySize)

	q.copy(data, len)

	if q.tail > q.head {
		q.rightMargin = q.tail
	}

	q.count++
}

关于rightMargin,用于标识队列中最后一个元素的位置,是一个绝对位置。所以,当队列需要扩容时,会copy该坐标之前的所有元素,如下面的示例代码。对于最正常的情况,该值跟tail相等。

copy(q.array, oldArray[:q.rightMargin])
关于head和tail是一个相对的坐标,而且跟严格意义上队列的两个属性不一致。在queue中存储的元素有timestamp的部分,而head所指向的元素不一定是最早插入队列的元素,同理,tail指向的元素也不是最晚插入队列的元素。它们会因为循环而相互变动,只要的作用便是:推断是否可以合理的插入新的元素。

if q.tail < q.head {
	emptyBlobLen := q.head - q.tail - headerEntrySize
	q.push(make([]byte, emptyBlobLen), emptyBlobLen)
	q.head = leftMarginIndex
	//absoulate position to right margin
	q.tail = q.rightMargin
}

关于leftMarginIndex声明成一个常量,而且head默认从1开始。为什么要这样处理,注释给出的解释:

// Bytes before left margin are not used. Zero index means element does not exist in queue,
//useful while reading slice from index

关于申请新的空间,引入了minimumEmptyBlobSize,它占用36个byte。它其实占用了一个比实际需要大的多的空间。

minimumEmptyBlobSize = 32 + headerEntrySize

当tail和head间的空隙,不足以容纳当前要插入的元素的时候,期间需要插入一个空的元素,具体到下面的代码:

emptyBlobLen := q.head - q.tail - headerEntrySize
q.push(make([]byte, emptyBlobLen), emptyBlobLen)

这个赋值的意义在这行代码才体现出来,当申请空间的时候,需要一个默认的值来标识:是否可以申请空间了。那么availableSpaceBeforeHead是可能产生负数的。

func (q *BytesQueue) availableSpaceBeforeHead() int {
	if q.tail >= q.head {
		//leftMarginIndex mean
		return q.head - leftMarginIndex - minimumEmptyBlobSize
	}
	return q.head - q.tail - minimumEmptyBlobSize
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/534699.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript:new操作符

一、new操作符的作用 用于创建一个给定构造函数的实例对象 new操作符创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例。二、new一个构造函数的执行过程 2.1、创建一个空对象obj 2.2、将空对象的原型与构造函数的原型连接起来 2.3、将构造函数中的this绑定…

CPU的功能和组成

CPU的功能和组成 CPU是控制计算机自动完成取指令和执行指令任务的部件&#xff0c;是计算机的核心部件、简称CPU 功能&#xff1a; 指令控制&#xff1a;对程序的顺序控制也是对指令执行的顺序控制&#xff08;PC、JMP&#xff09;操作控制&#xff1a;产生各种操作信号&…

2023.05.17-使用Vizzy进行音乐的可视化

文章目录 1. 简介2. 官网3. 使用3.1. 进行音乐可视化 1. 简介 Vizzy是MusicVid创作者的另一个在线音乐可视化工具。虽然这款应用还处于Alpha版本&#xff0c;但Vizzy相当令人印象深刻&#xff0c;绝对值得一试。Vizzy支持动画文本对象、频谱、图像和效果。工具集的最突出的功能…

基础篇007. 串行通信(二)--中断方式接收数据

目录 1. 实验任务 2. 硬件原理 3. 利用STM32CubeMX创建MDK工程 3.1 STM32CubeMX工程创建 3.2 配置调试方式 3.3 配置时钟电路 3.4 配置GPIO 3.5 配置串口参数 3.6 配置时钟 3.7 项目配置 4. 串行通信实验 4.1 UART串口printf&#xff0c;scanf函数串口重定向 4.2 …

数组(C语言程序设计)

一、一维数组 数组是相同类型的有序数据的集合 1、一维数组的定义 形式&#xff1a;类型名 数组名[常量表达式] 2、一维数组元素的引用 形式&#xff1a;数组名[下标] 3、一维数组的初始化 形式&#xff1a;类型名 数组名[数组长度]{初值表} 4、一维数组程序设计示例 【例6.…

如何画类图

是为了写论文才回头看的,已经忘光了 在类图中&#xff0c;我们用一个矩形来表示一个类。这个矩形通常分为三个部分&#xff1a; 顶部&#xff1a;写类的名字。 中间&#xff1a;写类的特性&#xff0c;比如一个"狗"类可能有"颜色"&#xff0c;“品种"…

Go语言的错误和异常处理:error、panic和recover

目录 【error类型】 error的基本用法 error.Is 用法 封装自定义错误结构体 error.As 用法 错误行为特征检视策略 【异常panic和recover】 panic recover panic 和 os.Exit 如何正确应对panic 【error类型】 error的基本用法 在Go语言中&#xff0c;一般使用 error …

【P1003 [NOIP2011 提高组] 铺地毯】

[NOIP2011 提高组] 铺地毯 题目描述 为了准备一个独特的颁奖典礼&#xff0c;组织者在会场的一片矩形区域&#xff08;可看做是平面直角坐标系的第一象限&#xff09;铺上一些矩形地毯。一共有 n n n 张地毯&#xff0c;编号从 1 1 1 到 n n n。现在将这些地毯按照编号从小…

Redis单线程 Vs 多线程

Redis单线程 Vs 多线程 一 面试题引入1.1 Redis到底是单线程还是多线程&#xff1f;1.2 IO多路复用1.3 Redis为什么快&#xff1f;1.4 Subtopic 二 Redis为什么选择单线程&#xff1f;2.1 这种问法其实并不严谨&#xff0c;为啥这么说&#xff1f;2.2 Redis是单线程究竟何意&am…

什么是JavaScript?为什么需要学习它?

JavaScript是一种广泛使用的编程语言&#xff0c;它被用于开发Web应用程序、桌面应用程序和移动应用程序。它的出现可以追溯到1995年&#xff0c;由瑞典计算机科学家Tim Bergling和美国计算机科学家John Resig共同开发。 JavaScript的历史可以追溯到20世纪90年代&#xff0c;当…

完美解决:由于找不到MSVR100.dll ,无法继续执行代码

当我们在运行某一个软件时&#xff0c;突然提示找不到MSVCR100.dll&#xff0c;我相信有不少用户都遇到过这种情况&#xff0c;并且在重新安装软件后还是无法解决。那么电脑提示找不到MSVCR100.dll该怎办呢? MSVCR100.dll是什么&#xff1f; 在解决找不到MSVCR100.dll这个问…

RabbitMQ之交换机详解

1 Exchages ​ 我们假设的是工作队列背后&#xff0c;每个任务都恰好交付给一个消费者(工作进程)。在这一部分中&#xff0c;我们将做一些完全不同的事情&#xff0c;我们将消息传达给多个消费者。这种模式 称为 ”发布/订阅“。 ​ 为了说明这种模式&#xff0c;我们将构建一…

vi和vim编辑器介绍与使用

VI 和 VIM 编辑器是 Unix 和 Linux 操作系统中最常用的文本编辑工具之一。虽然它们都用于编辑文本文件&#xff0c;但它们有一些不同之处。本文将对 VI 和 VIM 编辑器进行介绍&#xff0c;帮助你更好地了解编辑器的特性和优点。 Linux下常见的文本编辑器有&#xff1a; emacsp…

Unity解决在摄像机上面设置了TargetTexture后获取屏幕坐标不准的问题

大家好&#xff0c;我是阿赵 这里来分享一个最近遇到的小问题。 一、发现问题 如果我们想将3D模型放在UI上&#xff0c;一个比较普遍的做法是&#xff1a; 用一个单独的摄像机&#xff0c;把3D模型拍下来&#xff0c;并转成RenderTexture&#xff0c;贴到RawImage上。 那么如…

枚举类型enum详解

概述 enum是C语言中的一个关键字&#xff0c;enum叫枚举数据类型&#xff0c;枚举数据类型描述的是一组整型值的集合&#xff08;这句话其实不太妥当&#xff09;&#xff0c;因为枚举类型是一种基本数据类型&#xff0c;而不是一种构造类型&#xff0c;它不能再分解成什么基本…

架构设计如何绘图?

大家好&#xff0c;我是易安&#xff01; 很多同学技术能力很强&#xff0c;架构设计也做得很好&#xff0c;但是在给别人讲解的时候&#xff0c;总感觉像是“茶壶里煮饺子&#xff0c;有货倒不出”。 其实&#xff0c;在为新员工培训系统架构、给领导汇报技术规划、上技术大会…

详解c++STL—容器vector

一、vector基本概念 功能&#xff1a; vector数据结构和数组非常相似&#xff0c;也称为单端数组 vector与普通数组的区别&#xff1a; 不同之处在于数组是静态空间&#xff0c;而vector可以动态扩展 动态扩展&#xff1a; 并不是在原空间之后续接新空间&#xff0c;而是找…

阿里云ECS服务器实例挂载数据盘步骤

1、首先登录阿里云ECS服务器&#xff0c;通过 df -h 命令查看当前磁盘挂载情况&#xff0c;可以发现只有系统盘40G 2、通过 fdisk -l 命令查看磁盘情况&#xff0c;可以发现有两个盘&#xff1a; 系统盘 /dev/vda: 42.9GB&#xff0c; 数据盘 /dev/vdb: 322.1GB 3、运行 fdisk…

c++进阶项目—基于多态的职工管理系统

一、管理系统需求 公司中职工分为三类&#xff1a;普通员工、经理、老板 显示信息时&#xff0c;需要显示职工编号、职工姓名、职工岗位、以及职责 1、普通员工职责&#xff1a;完成经理交给的任务 2、经理职责&#xff1a;完成老板交给的任务&#xff0c;并下发任务给员工…

“Shell“正则表达式;文本三剑客Sed

文章目录 一.正则表达式1.1正则表达式介绍1.2命令演示1.3正则表达式实验演示 二.Sed编辑器2.1Sed介绍2.2操作命令演示2.3替换2.4插入2.4总结&#xff1a; 一.正则表达式 1.1正则表达式介绍 通常用于判断语句中&#xff0c;用来检查某一字符串是否满足某一格式 正则表达式是由…