深入解析缓存技术

文章目录

    • 1. 缓存基本原理
    • 2. 缓存更新机制
      • 2.1 Cache Aside模式
      • 2.2 Read/Write Through
      • 2.3 Write Behind Caching
      • 2.4 对比总结
    • 3. 缓存数据过期策略
      • 3.1 最近最少使用(Least Recently Used, LRU)算法
      • 3.2 先进先出(First-In-First-Out, FIFO)算法
      • 3.3 最不经常使用(Least Frequently Used, LFU)算法
    • 4. 缓存带来的问题和解决方案
      • 4.1 不一致性问题
      • 4.2 大key问题
      • 4.3 缓存雪崩
      • 4.4 缓存穿透
      • 4.5 缓存击穿
    • 总结

在现代Web应用中,缓存技术是提高系统性能和用户体验的关键手段之一。合理地使用缓存不仅可以减少数据库的负担,还能显著加快页面加载速度。本文将详细介绍缓存的基本原理、常见的缓存更新机制、数据过期策略以及缓存可能带来的一些问题及其解决方案,并通过Go语言代码示例来说明具体实现。

1. 缓存基本原理

缓存是一种临时存储机制,用于保存计算成本较高的数据副本,以便后续请求可以直接从缓存中获取这些数据而无需重新计算或查询数据库。简而言之,缓存通过减少对原始数据源的访问次数来加速数据访问过程。缓存可以位于应用程序的不同层级,如客户端浏览器、CDN节点、应用服务器内存等。

2. 缓存更新机制

2.1 Cache Aside模式

特点

  • 应用程序负责维护缓存和数据库之间的一致性。
  • 读取时,先检查缓存,如果缓存中没有数据,则从数据库加载并更新缓存。
  • 更新时,先更新数据库,然后使缓存失效或更新缓存。

优点

  • 缓存和数据库的耦合度较低,易于理解和实现。
  • 可以灵活地处理缓存未命中的情况。

缺点

  • 需要应用程序处理缓存和数据库之间的同步问题,增加了复杂性。
  • 在高并发情况下,可能会出现缓存和数据库之间的不一致,也可能导致缓存击穿。
读取数据
缓存命中
缓存未命中
返回数据
更新数据
应用程序
缓存
返回数据
从数据库读取数据
更新数据库
使缓存失效/更新缓存

Go语言实现

package mainimport ("fmt""sync""time"
)type Cache struct {data map[string]interface{}mu   sync.Mutex
}func NewCache() *Cache {return &Cache{data: make(map[string]interface{}),}
}func (c *Cache) Get(key string) (interface{}, bool) {c.mu.Lock()defer c.mu.Unlock()value, ok := c.data[key]return value, ok
}func (c *Cache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()c.data[key] = value
}func (c *Cache) GetFromDB(key string) (interface{}, error) {// 模拟从数据库获取数据time.Sleep(1 * time.Second)return fmt.Sprintf("Data for %s", key), nil
}func main() {cache := NewCache()key := "testKey"if value, ok := cache.Get(key); ok {fmt.Println("From Cache:", value)} else {value, err := cache.GetFromDB(key)if err != nil {fmt.Println("Error:", err)return}cache.Set(key, value)fmt.Println("From DB:", value)}
}

2.2 Read/Write Through

特点:

  • 缓存作为主要的数据访问层,负责处理所有数据的读取和写入操作。
  • 读取时,缓存负责从数据库加载数据并返回给应用程序。
  • 更新时,缓存负责将数据写入数据库,并更新自身的缓存。

优点:

  • 缓存和数据库的耦合度较高,但简化了应用程序的逻辑。
  • 减少了缓存未命中的情况,提高了数据访问的性能。

缺点:

  • 缓存成为性能瓶颈的可能性较大,特别是在高并发场景下。
  • 缓存故障可能导致数据丢失或不一致。
读取数据
缓存命中
缓存未命中
返回数据
更新数据
写入缓存
应用程序
缓存
返回数据
从数据库读取数据
写入数据库

Go语言实现

package mainimport ("fmt""sync""time"
)type Cache struct {data map[string]interface{}mu   sync.Mutex
}func NewCache() *Cache {return &Cache{data: make(map[string]interface{}),}
}func (c *Cache) Get(key string) (interface{}, bool) {c.mu.Lock()defer c.mu.Unlock()value, ok := c.data[key]return value, ok
}func (c *Cache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()c.data[key] = value
}func (c *Cache) GetFromDB(key string) (interface{}, error) {// 模拟从数据库获取数据time.Sleep(1 * time.Second)return fmt.Sprintf("Data for %s", key), nil
}func (c *Cache) SaveToDB(key string, value interface{}) error {// 模拟保存数据到数据库time.Sleep(1 * time.Second)return nil
}func main() {cache := NewCache()key := "testKey"if value, ok := cache.Get(key); ok {fmt.Println("From Cache:", value)} else {value, err := cache.GetFromDB(key)if err != nil {fmt.Println("Error:", err)return}cache.Set(key, value)fmt.Println("From DB:", value)}// 更新数据newValue := "New Data for testKey"cache.Set(key, newValue)if err := cache.SaveToDB(key, newValue); err != nil {fmt.Println("Error:", err)return}fmt.Println("Data updated successfully")
}

2.3 Write Behind Caching

特点:

  • 缓存作为主要的数据写入点,应用程序只与缓存交互。
  • 更新时,数据首先写入缓存,然后异步地批量写入数据库。
  • 读取时,如果缓存中没有数据,则从数据库加载并更新缓存。

优点:

  • 提高了写入性能,因为数据首先写入缓存,减少了直接写入数据库的开销。
  • 可以通过批量写入数据库来优化写入操作。

缺点:

  • 数据一致性问题更为复杂,因为存在缓存和数据库之间的延迟。
  • 缓存故障可能导致数据丢失,且恢复过程可能较为复杂。
更新数据
写入缓存
应用程序
缓存
异步写入数据库

Go语言实现

package mainimport ("fmt""sync""time"
)type Cache struct {data     map[string]interface{}updateCh chan stringmu       sync.Mutex
}func NewCache() *Cache {cache := &Cache{data:     make(map[string]interface{}),updateCh: make(chan string, 100),}go cache.updateWorker()return cache
}func (c *Cache) Get(key string) (interface{}, bool) {c.mu.Lock()defer c.mu.Unlock()value, ok := c.data[key]return value, ok
}func (c *Cache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()c.data[key] = valuec.updateCh <- key
}func (c *Cache) updateWorker() {for {keys := []string{}for i := 0; i < 10; i++ { // 批量处理10个更新select {case key := <-c.updateCh:keys = append(keys, key)default:time.Sleep(100 * time.Millisecond)}}if len(keys) > 0 {c.saveToDB(keys)}}
}func (c *Cache) saveToDB(keys []string) {for _, key := range keys {value := c.data[key]// 模拟保存数据到数据库fmt.Printf("Saving %s: %v to DB\n", key, value)time.Sleep(1 * time.Second)}
}func main() {cache := NewCache()key := "testKey"value := "Data for testKey"cache.Set(key, value)// 更新数据newValue := "New Data for testKey"cache.Set(key, newValue)// 等待一段时间,确保更新任务完成time.Sleep(3 * time.Second)fmt.Println("Data updated successfully")
}

2.4 对比总结

  • Cache Aside 模式适用于需要灵活处理缓存和数据库之间关系的场景,但需要应用程序自行维护一致性。
  • Read/Write Through 模式简化了应用程序的逻辑,但可能使缓存成为性能瓶颈。
  • Write Behind Caching 模式提高了写入性能,但增加了数据一致性的复杂性和风险。

3. 缓存数据过期策略

3.1 最近最少使用(Least Recently Used, LRU)算法

原理:根据数据最近的访问时间来决定哪些数据应该被淘汰。当缓存空间不足时,最长时间未被访问的数据将被移除。

优点

  • 能够有效利用有限的缓存空间,保留最近常用的热点数据。

缺点

  • 实现相对复杂,需要维护访问时间和链表结构。

Go语言实现

package mainimport ("container/list""fmt""sync"
)type LRUCache struct {capacity intdata     map[string]*list.Elementlist     *list.Listmu       sync.Mutex
}type cacheEntry struct {key   stringvalue interface{}
}func NewLRUCache(capacity int) *LRUCache {return &LRUCache{capacity: capacity,data:     make(map[string]*list.Element),list:     list.New(),}
}func (c *LRUCache) Get(key string) (interface{}, bool) {c.mu.Lock()defer c.mu.Unlock()if elem, ok := c.data[key]; ok {c.list.MoveToFront(elem)return elem.Value.(*cacheEntry).value, true}return nil, false
}func (c *LRUCache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()if elem, ok := c.data[key]; ok {c.list.MoveToFront(elem)elem.Value.(*cacheEntry).value = value} else {entry := &cacheEntry{key: key, value: value}elem := c.list.PushFront(entry)c.data[key] = elemif c.list.Len() > c.capacity {c.removeOldest()}}
}func (c *LRUCache) removeOldest() {oldest := c.list.Back()if oldest != nil {delete(c.data, oldest.Value.(*cacheEntry).key)c.list.Remove(oldest)}
}func main() {cache := NewLRUCache(3)cache.Set("a", 1)cache.Set("b", 2)cache.Set("c", 3)cache.Set("d", 4)if value, ok := cache.Get("a"); ok {fmt.Println("a:", value)} else {fmt.Println("a not found")}cache.Set("e", 5)if value, ok := cache.Get("b"); ok {fmt.Println("b:", value)} else {fmt.Println("b not found")}
}

3.2 先进先出(First-In-First-Out, FIFO)算法

原理:按照数据进入缓存的顺序来淘汰数据。最先加入缓存的数据会被优先移除。

优点

  • 实现简单,容易理解。

缺点

  • 可能会淘汰掉仍然频繁使用的数据。

Go语言实现

package mainimport ("container/list""fmt""sync"
)type FIFOCache struct {capacity intdata     map[string]*list.Elementlist     *list.Listmu       sync.Mutex
}type cacheEntry struct {key   stringvalue interface{}
}func NewFIFOCache(capacity int) *FIFOCache {return &FIFOCache{capacity: capacity,data:     make(map[string]*list.Element),list:     list.New(),}
}func (c *FIFOCache) Get(key string) (interface{}, bool) {c.mu.Lock()defer c.mu.Unlock()if elem, ok := c.data[key]; ok {return elem.Value.(*cacheEntry).value, true}return nil, false
}func (c *FIFOCache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()if elem, ok := c.data[key]; ok {elem.Value.(*cacheEntry).value = value} else {entry := &cacheEntry{key: key, value: value}elem := c.list.PushBack(entry)c.data[key] = elemif c.list.Len() > c.capacity {c.removeOldest()}}
}func (c *FIFOCache) removeOldest() {oldest := c.list.Front()if oldest != nil {delete(c.data, oldest.Value.(*cacheEntry).key)c.list.Remove(oldest)}
}func main() {cache := NewFIFOCache(3)cache.Set("a", 1)cache.Set("b", 2)cache.Set("c", 3)cache.Set("d", 4)if value, ok := cache.Get("a"); ok {fmt.Println("a:", value)} else {fmt.Println("a not found")}cache.Set("e", 5)if value, ok := cache.Get("b"); ok {fmt.Println("b:", value)} else {fmt.Println("b not found")}
}

3.3 最不经常使用(Least Frequently Used, LFU)算法

原理:根据数据被访问的频率来决定淘汰策略。访问次数最少的数据将被优先移除。

优点

  • 能够有效保留热点数据,提高缓存命中率。

缺点

  • 实现复杂,需要维护访问计数和多级链表结构。

Go语言实现

package mainimport ("container/list""fmt""sync"
)type LFUCache struct {capacity intdata     map[string]*list.ElementfreqList *list.Listmu       sync.Mutex
}type cacheEntry struct {key      stringvalue    interface{}freq     intfreqElem *list.Element
}type freqEntry struct {freq intlist *list.List
}func NewLFUCache(capacity int) *LFUCache {return &LFUCache{capacity: capacity,data:     make(map[string]*list.Element),freqList: list.New(),}
}func (c *LFUCache) Get(key string) (interface{}, bool) {c.mu.Lock()defer c.mu.Unlock()if elem, ok := c.data[key]; ok {entry := elem.Value.(*cacheEntry)c.promote(entry)return entry.value, true}return nil, false
}func (c *LFUCache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()if elem, ok := c.data[key]; ok {entry := elem.Value.(*cacheEntry)entry.value = valuec.promote(entry)} else {entry := &cacheEntry{key: key, value: value, freq: 1}c.addEntry(entry)if len(c.data) > c.capacity {c.removeLeastFrequent()}}
}func (c *LFUCache) promote(entry *cacheEntry) {oldFreqEntry := entry.freqElem.Value.(*freqEntry)oldFreqEntry.list.Remove(entry.elem)if oldFreqEntry.list.Len() == 0 {c.freqList.Remove(entry.freqElem)}newFreq := entry.freq + 1var newFreqEntry *list.Elementfor e := entry.freqElem.Next(); e != nil; e = e.Next() {freqEntry := e.Value.(*freqEntry)if freqEntry.freq >= newFreq {newFreqEntry = ebreak}}if newFreqEntry == nil {newFreqEntry = c.freqList.InsertAfter(&freqEntry{freq: newFreq, list: list.New()}, entry.freqElem)}entry.freq = newFreqentry.freqElem = newFreqEntryentry.elem = newFreqEntry.Value.(*freqEntry).list.PushFront(entry)
}func (c *LFUCache) addEntry(entry *cacheEntry) {if c.freqList.Front() == nil || c.freqList.Front().Value.(*freqEntry).freq != 1 {c.freqList.PushFront(&freqEntry{freq: 1, list: list.New()})}freqEntry := c.freqList.Front().Value.(*freqEntry)entry.freqElem = c.freqList.Front()entry.elem = freqEntry.list.PushFront(entry)c.data[entry.key] = entry.elem
}func (c *LFUCache) removeLeastFrequent() {leastFreqEntry := c.freqList.Front().Value.(*freqEntry)victim := leastFreqEntry.list.Back()if victim != nil {delete(c.data, victim.Value.(*cacheEntry).key)leastFreqEntry.list.Remove(victim)if leastFreqEntry.list.Len() == 0 {c.freqList.Remove(c.freqList.Front())}}
}func main() {cache := NewLFUCache(3)cache.Set("a", 1)cache.Set("b", 2)cache.Set("c", 3)cache.Set("d", 4)if value, ok := cache.Get("a"); ok {fmt.Println("a:", value)} else {fmt.Println("a not found")}cache.Set("e", 5)if value, ok := cache.Get("b"); ok {fmt.Println("b:", value)} else {fmt.Println("b not found")}
}

4. 缓存带来的问题和解决方案

4.1 不一致性问题

问题:缓存不一致性问题是指缓存中的数据与数据库中的数据不一致。

解决方案

  • 使用Read/Write Through模式。
  • 实现缓存预热机制,即在应用启动时预先加载常用数据到缓存中。
  • 设置合理的缓存过期时间。

4.2 大key问题

问题:大key问题指的是某些缓存键对应的值非常大,占用大量内存。

解决方案

  • 对大值进行分片存储。
  • 限制单个缓存项的最大大小。

4.3 缓存雪崩

问题:缓存雪崩是指大量缓存数据在同一时间过期,导致短时间内数据库压力剧增。

解决方案

  • 为不同缓存项设置不同的过期时间。
  • 使用分布式锁来控制缓存更新过程。

4.4 缓存穿透

问题:缓存穿透是指查询一个不存在的数据,由于缓存中没有该数据,每次请求都会直接查询数据库。

解决方案

  • 在缓存中存储空值,标记为已查询过。
  • 使用布隆过滤器等数据结构提前判断数据是否存在。

4.5 缓存击穿

问题:缓存击穿是指某个热点数据突然失效,大量请求直接打到数据库上。

解决方案

  • 使用互斥锁(mutex)来确保同一时间只有一个请求去数据库获取数据。
  • 预热缓存,定期刷新热点数据。

总结

缓存技术是提升Web应用性能的重要手段,但合理设计和使用缓存同样重要。通过了解缓存的基本原理、更新机制、数据过期策略以及常见问题的解决方案,可以帮助我们在实际项目中更好地利用缓存技术,构建高效稳定的系统。

希望本文对你有所帮助,如果你有任何问题或建议,欢迎留言交流!

关注我

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

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

相关文章

OpenCV高级图形用户界面(10)创建一个新的窗口函数namedWindow()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 创建一个窗口。 函数 namedWindow 创建一个可以作为图像和跟踪条占位符的窗口。创建的窗口通过它们的名字来引用。 如果已经存在同名的窗口&am…

linux线程 | 全面理解同步与互斥 | 同步

前言&#xff1a;本节内容主要讲解linux下的同步问题。 同步问题是保证数据安全的情况下&#xff0c;让我们的线程访问具有一定的顺序性。 线程安全就规定了它必须是在加锁的场景下的&#xff01;&#xff01;那么&#xff0c; 具体什么是同步问题&#xff0c; 我们加下来看看吧…

lesson01 Backtrader是什么

[Backtrader]专题连载 Backtrader是什么&#xff1f; Backtrader 是 2015 年开源的 Python 量化回测框架&#xff08;支持实盘交易&#xff09;。专注于为量化交易策略提供回测和实盘交易功能。它允许用户集中精力编写可复用的交易策略、指标和分析工具&#xff0c;而无需花费…

衡石分析平台系统分析人员手册-可视化报表仪表盘

仪表盘​ 仪表盘是数据分析最终展现形式&#xff0c;是数据分析的终极展现。 应用由一个或多个仪表盘展示&#xff0c;多个仪表盘之间有业务关联。 仪表盘编辑​ 图表列表​ 打开仪表盘后&#xff0c;就会看到该仪表盘中所有的图表。 调整图表布局​ 将鼠标移动到图表上拖动…

能源领域新政策,我们应该关注什么?

近日&#xff0c;国家发展改革委和国家能源局联合发布了《能源重点领域大规模设备更新实施方案》的通知。该方案指出&#xff0c;能源科技领域是大规模设备更新和消费品以旧换新行动实施的关键领域。 《方案》设定了到2027年&#xff0c;能源重点领域设备投资规模较2023年增长2…

CentOS 8 Stream环境下通过yum安装Mysql

1.在Mysql下载页面MySQL :: Download MySQL Community Server页尾 点击 “MD5 checksums and GnuPG signatures” 进入下一页面 2.打开下载yum repo文件页面 (MySQL :: Download MySQL Yum Repository) 3.点击"DownLoad"按钮&#xff0c;打开下载页面&#xff0c; 4.…

2012年国赛高教杯数学建模C题脑卒中发病环境因素分析及干预解题全过程文档及程序

2012年国赛高教杯数学建模 C题 脑卒中发病环境因素分析及干预 脑卒中&#xff08;俗称脑中风&#xff09;是目前威胁人类生命的严重疾病之一&#xff0c;它的发生是一个漫长的过程&#xff0c;一旦得病就很难逆转。这种疾病的诱发已经被证实与环境因素&#xff0c;包括气温和湿…

如何利用kafka实现高效数据同步?

在我们之前的文章有详细介绍过Kafka的结构、特点和处理方式。具备告诉处理能力的kafka被利用在数据同步和数据传输上&#xff0c;今天来说下kafka是怎么实现高效的数据同步和传输。 一、可靠的数据传输 1. 持久性保证&#xff1a;Kafka 将数据持久化到磁盘上&#xff0c;即使在…

深度学习实战94-基于图卷积神经网络GCN模型的搭建以及在金融领域的场景

大家好,我是微学AI,今天给大家介绍一下深度学习实战94-基于图卷积神经网络GCN模型的搭建以及在金融领域的场景。文章首先介绍了GCN模型的原理及模型结构,随后提供了数据样例,并详细展示了实战代码。通过本文,读者可以深入了解GCN模型在金融场景下的应用,同时掌握代码的具…

wifi、热点密码破解 - python

乐子脚本&#xff0c;有点小慢&#xff0c;试过多线程&#xff0c;系统 wifi 连接太慢了&#xff0c;需要时间确认&#xff0c;多线程的话系统根本反应不过来。 也就可以试试破解别人的热点&#xff0c;一般都是 123456 这样的傻鸟口令 # coding:utf-8 import pywifi from pyw…

BF 算法

目录 BF算法 算法思路 完整代码 时间复杂度 查找所有起始位置 BF算法 BF算法&#xff1a;即暴力(Brute Force)算法&#xff0c;是一种模式匹配算法&#xff0c;将目标串 S 的第一个字符与模式串 T 的第一个字符进行匹配&#xff0c;若相等&#xff0c;则继续比较 S 的第二…

【最新华为OD机试E卷-支持在线评测】TLV解码(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

【git】如何快速准确的回退(reverse)已经合并(merge)主分支(master)的新提交代码

文章目录 前言一、merge模式二、回滚步骤总结 前言 我们在做一些需求&#xff0c;正常流程经过开发&#xff0c;测试到最后和代码上线。但是有时候就会发生一些小插曲&#xff0c;比如产品说老板说某某某你的代码要延后上线&#xff01;&#xff01;或者你写的不合格预发环境出…

在Openshift(K8S)上通过EMQX Operator部署Emqx集群

EMQX Operator 简介 EMQX Broker/Enterprise 是一个云原生的 MQTT 消息中间件。 我们提供了 EMQX Kubernetes Operator 来帮助您在 Kubernetes 的环境上快速创建和管理 EMQX Broker/Enterprise 集群。 它可以大大简化部署和管理 EMQX 集群的流程&#xff0c;对于管理和配置的知…

ubuntu 安装keepalived+haproxy

一、安装keepalived sudo apt update sudo apt install keepalived sudo systemctl start keepalived sudo systemctl enable keepalived sudo systemctl status keepalived#配置Keepalived sudo cp /etc/keepalived/keepalived.conf.sample /etc/keepalived/keepalived.conf …

Java面试宝典-并发编程学习02

目录 21、并行与并发有什么区别&#xff1f; 22、多线程中的上下文切换指的是什么&#xff1f; 23、Java 中用到的线程调度算法是什么&#xff1f; 24、Java中线程调度器和时间分片指的是什么&#xff1f; 25、什么是原子操作&#xff1f;Java中有哪些原子类&#xff1f; 26、w…

Python案例小练习——小计算器

文章目录 前言一、代码展示二、运行展示 前言 这是用python实现一个简单的计器。 一、代码展示 def calculate(num1, op, num2):if op "":return float(num1) float(num2)elif op "-":return float(num1) - float(num2)elif op "*":return…

【Mac苹果电脑安装】DBeaverEE for Mac 数据库管理工具软件教程【保姆级教程】

Mac分享吧 文章目录 DBeaverEE 数据库管理工具 软件安装完成&#xff0c;打开效果图片Mac电脑 DBeaverEE 数据库管理工具 软件安装——v24.21️⃣&#xff1a;下载软件2️⃣&#xff1a;安装JDK&#xff0c;根据下图操作步骤提示完成安装3️⃣&#xff1a;安装DBeaverEE&#…

C++类域访问方式(public,protected,private)对象访问 , 通过成员函数访问 ,通过友元函数访问

c类的用法 yC 类的基础用法与详细说明&#xff1a;简单易懂的入门指南-CSDN博客 类的基本概念&#x1f447; 类是C中的一个用户定义的数据类型&#xff0c;它可以包含数据&#xff08;成员变量&#xff09;和函数&#xff08;成员函数&#xff09;。通过类&#xff0c;我们可以…

【Jenkins】windows安装步骤

【Jenkins】windows安装步骤 官网使用WAR包方式运行浏览器访问Jenkinswindows-installer安装安装过程问题解决This account either does not hava the privilege to logon as a service or the account was unable to be verified 安装成功修改jenkins.xml启动jenkins访问jenki…