本地缓存库分析(一):golang-lru
文章目录
- 本地缓存概览
- golang-lru
- 标准lru
- lru的操作
- Put
- Get
- 2q:冷热分离lru
- Put
- Get
- expirable_lru:支持过期时间的lru
- Put
- Get
- 过期
- 总结
本地缓存概览
在业务中,一般会将极高频访问的数据缓存到本地。以减少网络IO的开销,下游服务的压力,提高性能
一般来说能放到本地的数据需要满足下面两个限制:
- 数据量不是非常大:数据量大了本地内存撑不住
- 一致性,时效性要求不是非常高:毕竟多个服务的本地缓存很难做到同步更新,及时更新
如果用go自带的map实现本地缓存,大概有两种实现方式:
sync.Map
map + mutex.RWLock
但有以下缺点:
- 锁竞争严重
- 大量缓存写入,导致gc标记阶段占用cpu多
- 内存占用不可控
- 不支持缓存按时效性淘汰
- 不支持缓存过期
- 缓存数据可以被污染:如果缓存的V是指针,那么业务修改了V的某个值为当前请求用户自己的值,在缓存中的V就被污染了
本系列要介绍的开源缓存库如何解决上述问题?
-
将大map拆分成多个小map,每个小map使用各自的锁
-
零GC:
-
使用堆外内存,不把对象放到堆上,自然不会被gc扫描。但要注意手动管理,需要及时释放内存
-
map的非指针优化:
- 如果kv都没有指针,不会扫描map。注意常用作key的string类型含有指针,会被gc扫描
- 将hash值作为key,value在底层数组中的offset作为value,这样KV都是int,就不会被GC扫描了
-
-
内存占用可控:
- 初始化时制定好底层数组的容量,数据写满时会覆写,这样永远不会超过容量
- 或者指定好最多能放多少KV对,但如果V大小不一,极端情况下内存占用会很大
-
支持缓存按时效性淘汰,例如使用LRU算法
-
支持数据过期
- 某些库在后台启定时任务,定时清理过期的KV
- 某些库会在Get时,惰性检查KV是否过期
-
避免缓存污染:存储Value序列化后的字节数组,而不是指针
- 但cpu开销会增大,每次写入缓存都要经过序列化,每次从缓存读都要经过反序列化。内存开销也变大,每次读都相当于拷贝一份出来
- 也就是用性能换取安全性
golang-lru
本文阅读源码:https://github.com/hashicorp/golang-lru,版本:v2.0.7
该库提供了3种LRU的实现:
-
lru
:标准lru -
2q
:类似mysql的buffer pool,分为冷数据
和热数据
两部分。如果某对KV只被添加到缓存中,而没有被查询,那么只会待在冷数据区域直到被淘汰,而不会占用热数据的空间- 为啥要冷热分离?考虑这样一种场景:假设缓存中有10条热点数据,突然有用户往缓存中写10条冷数据,因为容量不够,要淘汰掉所有的热数据。如果之后也不查这些冷数据,而是继续查之前的热数据。产生的后果是所有热数据都会
cacheMiss
- 如果应用了冷热分离机制,这些冷数据只会写到冷数据区,然后在冷数据区被淘汰,而不会占用热数据的空间,避免了大量热数据的cacheMiss
- 为啥要冷热分离?考虑这样一种场景:假设缓存中有10条热点数据,突然有用户往缓存中写10条冷数据,因为容量不够,要淘汰掉所有的热数据。如果之后也不查这些冷数据,而是继续查之前的热数据。产生的后果是所有热数据都会
-
expirable_lru
:支持过期时间的lru
标准lru
数据结构:包含一个双向链表和hash表
type LRU[K comparable, V any] struct {// 容量size int// entry越靠近头部,越新evictList *internal.LruList[K, V]// hash表items map[K]*internal.Entry[K, V]onEvict EvictCallback[K, V]
}
LruList就是个带哨兵头节点root
的双向链表,每个节点是Entry结构。哈希表items
的value也是Entry机构
那么真正的头节点是root.Next
尾节点是root.Prev
type LruList[K comparable, V any] struct {// 哨兵entryroot Entry[K, V]// 已经放了多少entrylen int
}
entry结构如下:
type Entry[K comparable, V any] struct {// 前后指针next, prev *Entry[K, V]// 属于哪个list,主要用于遍历链表时,在lru算法中没啥用list *LruList[K, V]Key KValue V
}
初始化时root自己先形成一个环:
func (l *LruList[K, V]) Init() *LruList[K, V] {l.root.next = &l.rootl.root.prev = &l.rootl.len = 0return l
}
下面介绍一些在LRU算法中会用到的小方法
获取最后一个节点:
func (l *LruList[K, V]) Back() *Entry[K, V] {if l.len == 0 {return nil}return l.root.prev
}
将entry添加到头部:
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {l.lazyInit()return l.insertValue(k, v, time.Time{}, &l.root)
}
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
}
就是普通的双链表操作,将e插到at的后面
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {e.prev = ate.next = at.nexte.prev.next = ee.next.prev = ee.list = ll.len++return e
}
将entry移动到头部:
func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {if e.list != l || l.root.next == e {return}l.move(e, &l.root)
}
move方法:
func (l *LruList[K, V]) move(e, at *Entry[K, V]) {if e == at {return}// 先将e从list删除e.prev.next = e.nexte.next.prev = e.prev// 再将e插到at后面e.prev = ate.next = at.nexte.prev.next = ee.next.prev = e
}
lru的操作
主要看看Get
和Put
这两大流程
原则:每次操作完,都要将KV所在的entry移动到list头部,表示该entry实效性最好,最不应该过期
Put
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {// 如果已存在,将entry移到链表头部,更新valueif ent, ok := c.items[key]; ok {c.evictList.MoveToFront(ent)ent.Value = valuereturn false}// 新元素,加入头部ent := c.evictList.PushFront(key, value)c.items[key] = entevict := c.evictList.Length() > c.size// 如果容量超了,需要移除最老的if evict {c.removeOldest()}return evict
}
removeOldest:删除最老的entry
func (c *LRU[K, V]) removeOldest() {// 找到列表末尾的entryif ent := c.evictList.Back(); ent != nil {c.removeElement(ent)}
}// 把entry从链表和map中删除
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {c.evictList.Remove(e)delete(c.items, e.Key)if c.onEvict != nil {c.onEvict(e.Key, e.Value)}
}
Get
如果key存在,将key所在的entry移动到list头部
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {if ent, ok := c.items[key]; ok {c.evictList.MoveToFront(ent)return ent.Value, true}return
}
2q:冷热分离lru
2q在用了3个lru
recent
:保存冷数据,默认容量为size的1/4frequent
:保存热数据,默认容量为size的3/4recentEvict
:保存最近从冷数据中被删除的key,默认容量为size的1/2
数据加到缓存时,首先被添加到冷数据区,如果后续没有操作,就会在冷数据区被淘汰。如果在还没被淘汰时执行了Put或Get,就会提升到热数据区
在Put时,如果某个key在冷热数据都没有,但在recentEvict中有,说明是最近被删除的,也当做热数据处理,加到冷数据区
type TwoQueueCache[K comparable, V any] struct {size int// size * 0.25recentSize int// 0.25recentRatio float64// 0.5ghostRatio float64// 保存冷数据recent simplelru.LRUCache[K, V]// 正常热数据frequent simplelru.LRUCache[K, V]// 最近从冷数据删除的keyrecentEvict simplelru.LRUCache[K, struct{}]lock sync.RWMutex
}
构造方法,就是初始化3个lru,以及每个lru的容量
const (// 默认冷数据的容量 = size * 0.25Default2QRecentRatio = 0.25// 默认被删除数据的容量 = size * 0.5Default2QGhostEntries = 0.50
)func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
}func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {recentSize := int(float64(size) * recentRatio)evictSize := int(float64(size) * ghostRatio)// 初始化3个lrurecent, err := simplelru.NewLRU[K, V](size, nil)if err != nil {return nil, err}frequent, err := simplelru.NewLRU[K, V](size, nil)if err != nil {return nil, err}recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)if err != nil {return nil, err}c := &TwoQueueCache[K, V]{size: size,recentSize: recentSize,recentRatio: recentRatio,ghostRatio: ghostRatio,recent: recent,frequent: frequent,recentEvict: recentEvict,}return c, nil
}
重点还是看Put和Get
Put
流程为:
- 如果key在热数据中有,那就在热数据的lru中执行Put,更新其value,返回
- 如果在冷数据中有,那么本次不是第一次操作,说明该key不再是冷数据了,将其移动到热数据的lru中,返回
- 到这一步说明在冷热两个lru中都没有,再看看
recentEvict
中有没有,如果有,说明是最近才从冷数据被删除的,那么也算作是热数据,在热数据lru中新增 - 否则就在冷数据lru中新增
func (c *TwoQueueCache[K, V]) Add(key K, value V) {c.lock.Lock()defer c.lock.Unlock()// 如果在热数据中有,在热数据的lru中执行Putif c.frequent.Contains(key) {c.frequent.Add(key, value)return}// 如果在冷数据中有,移动到frequent中if c.recent.Contains(key) {c.recent.Remove(key)c.frequent.Add(key, value)return}// 在两个lru中都没有,但最近移除过这个key,加到frequent中if c.recentEvict.Contains(key) {c.ensureSpace(true)c.recentEvict.Remove(key)c.frequent.Add(key, value)return}// 否则加到recent中c.ensureSpace(false)c.recent.Add(key, value)
}
当需要新增时,需要确保容量足够,如果容量超了,需要淘汰老数据,给新数据腾位置
ensureSpace
方法干这个活,淘汰规则为:
-
如果recent和frequent的len加起来不够size,判定为还有容量,不淘汰
- 也就是说,在容量没满时,冷热数据区分别都可以用到size个空间,有很大的灵活性
-
否则看冷数据区frequent有没有超过容量限制,超过了就从frequent中淘汰一个
-
否则从热数据区中淘汰一个
func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {// 如果还有空间,返回recentLen := c.recent.Len()freqLen := c.frequent.Len()if recentLen+freqLen < c.size {return}/**recent超过了限制, 从recent移除最老的entry,将key加到recentEvict中*/if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {k, _, _ := c.recent.RemoveOldest()c.recentEvict.Add(k, struct{}{})return}// 否则就是frequent超过限制了,从frequent中移除最老的entryc.frequent.RemoveOldest()
}
Get
- 先看热数据有没有该key,如果有返回对应的value
- 再看冷数据有没有,如果有,说明本次不是第一次操作该key,将其提升到热数据中
- 否则在冷热数据中都没有,返回空
func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {c.lock.Lock()defer c.lock.Unlock()// 先看热数据有没有if val, ok := c.frequent.Get(key); ok {return val, ok}// 在看冷数据有没有if val, ok := c.recent.Peek(key); ok {c.recent.Remove(key)c.frequent.Add(key, val)return val, ok}// No hitreturn
}
expirable_lru:支持过期时间的lru
其结构就是在标准LRU的基础上,增加过期时间ttl和过期桶(固定为100个)
所有KV都应用相同的过期时间ttl
每次Put后,会把key加到最新的过期桶中
后台有定时任务,每ttl/100
时间执行一次,把即将过期的桶nextCleanupBucket
中的数据清空
type LRU[K comparable, V any] struct {// 标准LRU结构size intevictList *internal.LruList[K, V]items map[K]*internal.Entry[K, V]onEvict EvictCallback[K, V]mu sync.Mutex// LRU中的所有kv都用这个过期时间ttl time.Durationdone chan struct{}// 存储所有过期的key,buckets []bucket[K, V]// 下次要清除的bucket索引nextCleanupBucket uint8
}
bucket定义如下
type bucket[K comparable, V any] struct {// 所有过期的keyentries map[K]*internal.Entry[K, V]// enteied中的所有key,最晚在啥时候过期newestEntry time.Time
}
Put
在标准LRU的基础上,新增了对过期桶的操作
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {c.mu.Lock()defer c.mu.Unlock()now := time.Now()// key存在if ent, ok := c.items[key]; ok {c.evictList.MoveToFront(ent)// 将entry从过期桶移除c.removeFromBucket(ent)ent.Value = valueent.ExpiresAt = now.Add(c.ttl)// 加入最新的过期桶c.addToBucket(ent)return false}// key不存在ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))c.items[key] = entc.addToBucket(ent)// 容量超了,移除最老的元素evict := c.size > 0 && c.evictList.Length() > c.sizeif evict {c.removeOldest()}return evict
}
看看怎么将entry加入过期桶:
func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {// 加到nextCleanupBucket-1对应的bucket里,也就是最新的bucketbucketID := (numBuckets + c.nextCleanupBucket - 1) % numBucketse.ExpireBucket = bucketIDc.buckets[bucketID].entries[e.Key] = e// 更新桶中最新的entry过期时间if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {c.buckets[bucketID].newestEntry = e.ExpiresAt}
}
解释下为啥加到下标为nextCleanupBucket-1的桶里:nextCleanupBucket为即将失效的桶,那么nextCleanupBucket-1就是在当前时刻来说,最晚失效的桶
Get
在标准LRU的Get流程上,多了一步校验key是否过期
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {c.mu.Lock()defer c.mu.Unlock()var ent *internal.Entry[K, V]if ent, ok = c.items[key]; ok {// 校验是否过期if time.Now().After(ent.ExpiresAt) {return value, false}c.evictList.MoveToFront(ent)return ent.Value, true}return
}
过期
缓存数据怎么实现过期呢?在初始化LRU时,起了后台任务:
go func(done <-chan struct{}) {ticker := time.NewTicker(res.ttl / numBuckets)defer ticker.Stop()for {select {case <-done:returncase <-ticker.C:res.deleteExpired()}}
}(res.done)
每隔一段时间执行deleteExpired
方法:
- 准备将下标为nextCleanupBucket的中的所有KV过期
- sleep直到时间到
newestEntry
,因为桶中最晚过期的key在这个时候,不能提前过期 - 将该桶中所有KV删除
- 推进nextCleanupBucket,让
nextCleanupBucket++
func (c *LRU[K, V]) deleteExpired() {c.mu.Lock()// bucketIdx := c.nextCleanupBuckettimeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)// sleep直到newestEntry到来if timeToExpire > 0 {c.mu.Unlock()time.Sleep(timeToExpire)c.mu.Lock()}// 将里面所有kv删除for _, ent := range c.buckets[bucketIdx].entries {c.removeElement(ent)}// 推进nextCleanupBucketc.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBucketsc.mu.Unlock()
}
总结
最后看看golang-lru解决了哪些原生缓存的问题:
问题 | 解决 |
---|---|
锁竞争严重 | 没有解决,只有一把大锁,锁竞争依然严重 |
大量缓存写入,导致gc标记阶段占用cpu多 | 没有解决 |
内存占用不可控 | 有改善,在KV个数的层面可用,在总内存占用量的层面依然不可用 |
不支持缓存按时效性淘汰 | 解决了,支持按LRU算法淘汰 |
不支持缓存过期 | 解决了,expirable_lru支持 |
缓存数据可以被污染 | 没有解决,还是存指针 |
相关文章:
本地缓存库分析(一):golang-lru
文章目录 本地缓存概览golang-lru标准lrulru的操作PutGet 2q:冷热分离lruPutGet expirable_lru:支持过期时间的lruPutGet过期 总结 本地缓存概览 在业务中,一般会将极高频访问的数据缓存到本地。以减少网络IO的开销,下游服务的压…...
qt配置https请求
qt应用版本 windows 32位 先说下心理路程,你能遇到的我都遇到了,你能想到的我都想到了,怎么解决看这一篇就够了,从上午12点到晚上12点几乎没离开电脑(除了吃饭),对于openssl这种用的时候无感&am…...
C语言进阶——文件操作
一、文件的基本知识 1.1什么是文件 在程序设计中,一般谈的文件有两种:程序文件、数据文件。 程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执…...
MYSQL-查看用户权限语法(二十一)
13.7.5.21 SHOW GRANTS 语句 SHOW GRANTS [FOR user]此语句以GRANT语句的形式显示分配给MySQL用户帐户的权限,必须执行GRANT语句才能复制权限分配。 注意 要显示MySQL帐户的非特权信息,请使用SHOW CREATE USER语句。 参见第 13.7.5.12 节“ SHOW CREA…...
在MySQL中存储IP地址的最佳实践
文章目录 一、IP地址的格式二、存储IP地址的数据类型选择1. VARCHAR优点缺点 2. INT 或 BIGINT优点缺点示例 3. VARBINARY优点缺点示例 三、最佳实践建议1. 选择合适的数据类型2. 索引优化3. 数据验证4. 安全性考虑 四、Java支持五、结论 在现代网络应用中,IP地址是…...
Vite打包配置
Vite打包配置 1.项目启动自动打开网页 {"scripts": {"dev": "vite --open"} }2.base配置打包公共路径 配置base选项的作用主要是指定项目在开发或生产环境中的公共基础路径。这个配置项对于确保资源能够正确加载尤为关键,尤其是在…...
node集成redis (教学)
文章目录 前言一、安装redis二、可视化界面测试连接1.vscode安装插件 三、node代码编写1.先安装两个库(redis和ioredis)2.测试连接 (前提是你的redis服务器要启动起来) 总结 前言 在Node.js中集成ioredis是一个常见的做法&#x…...
江协科技STM32学习- P22 实验-ADC单通道/ADC多通道
🚀write in front🚀 🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝…...
RL学习笔记-马尔可夫过程
参考资料:蘑菇书、周博磊老师课程 在强化学习中,智能体与环境交互是通过马尔可夫决策过程来表示的,因此马尔可夫决策过程是强化学习的基本框架。 马尔可夫性质 指一个随机过程在给定现在状态及所有过去状态情况下,其未来状态的条件…...
LeetCode Hot 100:动态规划
LeetCode Hot 100:动态规划 70. 爬楼梯 class Solution { public:int climbStairs(int n) {if (n 0)return 0;vector<int> dp(n 1);// 初始化dp[0] 1;// 状态转移for (int i 1; i < n; i) {dp[i] dp[i - 1];if (i > 2)dp[i] dp[i - 2];}return …...
使用Python制作雪景图片教程
如果你想用Python写一个程序来输出有关“深夜雪”的诗意文本或描述,可以通过简单的字符串输出来实现。以下是一个示例代码,展示如何用Python来描绘深夜雪的场景。 # 定义深夜雪的描述 description """ 夜幕降临,天空洒下银色…...
S-Function
目录 S-Function介绍 生成S-Function的三种常用手段 使用手写S-函数合并定制代码 使用S-Function Builder块合并定制代码 使用代码继承工具合并定制代码 S-Function介绍 我们可以使用S-Function扩展Simulink对仿真和代码生成的支持。例如,可以使用它们…...
如何具备阅读JAVA JDK虚拟机源码能力
源码位置https://github.com/openjdk/jdk 核心实现源码[部分截图] /* * Copyright (c) 1995, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistr…...
Python | Leetcode Python题解之第514题自由之路
题目: 题解: Test "godding" target "d"i 0left i lc 0 right i rc 0while Test[left] ! target:left - 1lc 1if left -1:left len(Test) - 1while Test[right] ! target:right 1rc 1if right len(Test):right 0prin…...
Docker 镜像下载问题及解决办法
Docker 镜像下载问题及解决办法 我在杂乱的、破旧的村庄寂寞地走过漫长的雨季,将我年少的眼光从晦暗的日子里打捞出来的是一棵棵开花的树,它们以一串串卓然不俗的花擦明了我的眼睛,也洗净了我的灵魂。 引言 在使用 Docker 时,用户…...
2分钟搞定 HarmonyOs Next创建模拟器
官方文档参考链接: 创建模拟器-管理模拟器-使用模拟器运行应用/服务-应用/服务运行-DevEco Studio - 华为HarmonyOS开发者https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-emulator-create-V5 1. 首先打开Device Manager 2. 进入这个界面后…...
方形件排样优化与订单组批问题探析
方形件排样优化与订单组批问题是计算复杂度很高的组合优化问题,在工业工程中有很广泛的应用背景。为实现个性化定制生产模式,企业会选择订单组批的方式,继而通过排样优化实现批量切割,加工完成后再按照不同客户需求进行分拣&#…...
vue3组件通信--自定义事件
自定义事件是典型的子传父的方法。 为什么叫自定义事件呢?是因为我们用sendToy"getToy"这种格式写,很显然,在DOM中,没有叫sendToy的事件。 父组件FatherComponent.vue: <script setup> import ChildComponent fr…...
ubuntu 安装k3s
配置hostname的方法为 hostnamectl set-hostname k3sserver hostnamectlsudo apt-get update && sudo apt-get upgrade -y sudo apt-get install -y curl#手动下载v1.31.1k3s1 https://github.com/k3s-io/k3s/releases/tag/v1.31.1%2Bk3s1 #将k3s-airgap-images-amd64…...
SQL CHECK 约束:确保数据完整性的关键
SQL CHECK 约束:确保数据完整性的关键 在数据库管理中,确保数据的完整性和准确性是至关重要的。SQL(Structured Query Language)提供了多种约束条件来帮助实现这一目标,其中之一就是 CHECK 约束。本文将深入探讨 SQL CHECK 约束的概念、用法和优势,并展示如何在不同的数…...
C++ | Leetcode C++题解之第502题IPO
题目: 题解: typedef pair<int,int> pii;class Solution { public:int findMaximizedCapital(int k, int w, vector<int>& profits, vector<int>& capital) {int n profits.size();int curr 0;priority_queue<int, vect…...
《虚拟现实的边界:探索虚拟世界的未来可能》
内容概要 在虚拟现实(VR)技术的浪潮中,我们见证了其从实验室的奇想逐渐走向日常生活的非凡旅程。技术发展的背后是不断突破的创新,早期的设备虽然笨重,但如今却趋向精致、轻巧,用户体验显著提升。想象一下…...
Rust教程
2024 Rust现代实用教程:1.1Rust简介与安装更新––2024 Rust现代实用教程:1.2编译器与包管理工具以及开发环境–––––––––––...
测试代理IP的有效性和可用性
使用代理IP的有效性和可用性直接关系到用户的工作效率,尤其是在进行数据抓取、网络爬虫和保护个人隐私等场景中。 一、测试代理IP的必要性 代理IP的可用性测试是确保代理服务正常运行的重要步骤。测试代理IP的必要性主要体现在以下几个方面: 提升工作…...
散列表:为什么经常把散列表和链表放在一起使用?
散列表:为什么经常把散列表和链表放在一起使用? 在计算机科学中,散列表(哈希表)和链表是两种常见的数据结构。你可能会好奇,为什么它们经常被放在一起使用呢?让我们一起来深入探讨这个问题。 一、散列表的特点 散列表是一种根据关键码值(Key value)而直接进行访问的…...
计算机网络:网络层 —— IPv4 地址与 MAC 地址 | ARP 协议
文章目录 IPv4地址与MAC地址的封装位置IPv4地址与MAC地址的关系地址解析协议ARP工作原理ARP高速缓存表 IPv4地址与MAC地址的封装位置 在数据传输过程中,每一层都会添加自己的头部信息,最终形成完整的数据包。具体来说: 应用层生成的应用程序…...
PMP--一、二、三模、冲刺、必刷--分类--10.沟通管理--技巧--文化意识
文章目录 技巧一模10.沟通管理--1.规划沟通管理--文化意识--军事背景和非军事背景人员有文化差异文化意识:题干关键词 “两拨人的背景不同、文化差异、风格差异”。5、 [单选] 项目团队由前军事和非军事小组成员组成。没有军事背景的团队成员认为前军事团队成员在他…...
FileReader和FileWriter
FileReader 使用read()方法读取单个字符,下面是如何修改使程序性能更好的过程。 第一种:处理异常方式为throws Testpublic void test() throws IOException {//读取hello.txt,并显示内容// 创建文件对象File file new File("hello.txt…...
【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第六篇-阶段总结篇】
因为马上就要进入下一个阶段,制作动态编辑体积纹理的模块。 但在这之前,要在这一章做最后一些整理。 首先,我们完成没完成的部分。其次,最后整理一下图表。最后,本文附上正在用的贴图 完善Shader 还记得我们之前注…...
地球村上一些可能有助于赚钱的20个思维方式
地球村上一些可能有助于赚钱的20个思维方式: 1. 目标导向思维:明确自己的财务目标,并制定详细、可执行的计划来逐步实现。 2. 创新思维:不断寻求新的商业机会和独特的解决方案,以在竞争激烈的市场中脱颖而出。 3. 价值…...
长垣县住房和城乡建设局网站/宁波企业seo推广
随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666) 有时候,程序需要管理系统音量,或者直接让系统静音,这就可以借助AudioManager来实现。在通过getSystemService(Service.AUDIO_…...
b2b官网seo/seo接单
1. Windows SEH 与 C Exception 1) Windows SEH 结构化异常 结构化异常是Windows 操作系统提供的与语言无关的异常处理机制, SHE使用Win32API中的RaiseException()函数来抛出异常,在VC中使用关键字__try和关键字__except来…...
做微课常用的网站有哪些/如何交换优质友情链接
香蕉派 Banana Pi BPI-M2四核开源开发板 全志H3芯片方案,也可以支持Allwinner H2 ,H3,H5芯片。Banana Pi BPI-M2Banana PI BPI-M2 是一款四核高性能单板计算机,采用全志H3处理器。Banana PI BPI-M2兼容性强大,可以跑android系统,Debian linux…...
查飞机进出港的app/网站seo外包公司
背景: 我们有个车管系统,需要定期的去查询车辆的违章,之前一直是调第三方接口去查,后面发现数据不准确(和深圳交警查的对不上),问题比较多。于是想干脆直接从深圳交警上查,那不就不会…...
网站建设预算表/公司推广渠道
转自:http://www.yopoing.com/2011/12/secret_sql_database_performance_optimization_techniques_to_improve/ 这篇文章是以 MySQL 为背景,很多内容同时适用于其他关系型数据库,需要有一些索引知识为基础,重点讲述如何优化SQL,来…...
泉州模板做网站/教育培训机构排名
面试跳槽 说到面试跳槽,大家从当初入行开始就一直摆脱不开它(咱们就是通过不断跳槽才能更快地提升自己)。在我们的技术生涯中会有很多大大小小的面试,对我们程序员来说每一次面试都是一次提升的机会,不管是简历修改&a…...