本地缓存库分析(一):golang-lru

文章目录
- 本地缓存概览
- golang-lru
- 标准lru
- lru的操作
- Put
- Get
- 2q:冷热分离lru
- Put
- Get
- expirable_lru:支持过期时间的lru
- Put
- Get
- 过期
- 总结
本地缓存概览
在业务中,一般会将极高频访问的数据缓存到本地。以减少网络IO的开销,下游服务的压力,提高性能
一般来说能放到本地的数据需要满足下面两个限制:
- 数据量不是非常大:数据量大了本地内存撑不住
- 一致性,时效性要求不是非常高:毕竟多个服务的本地缓存很难做到同步更新,及时更新
如果用go自带的map实现本地缓存,大概有两种实现方式:
sync.Mapmap + 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 约束的概念、用法和优势,并展示如何在不同的数…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...
高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
