热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何提高进程内缓存的并发

本篇文章给大家分享的是有关如何提高进程内缓存的并发,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说

本篇文章给大家分享的是有关如何提高进程内缓存的并发,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

缓存,设计的初衷是为了减少繁重的IO操作,增加系统并发能力。不管是 CPU多级缓存page cache,还是我们业务中熟悉的 redis 缓存,本质都是将有限的热点数据存储在一个存取更快的存储介质中。

计算机本身的缓存设计就是 CPU 采取多级缓存。那对我们服务来说,我们是不是也可以采用这种多级缓存的方式来组织我们的缓存数据。同时 redis 的存取都会经过网络IO,那我们能不能把热点数据直接存在本进程内,由进程自己缓存一份最近最热的这批数据呢?

这就引出了我们今天探讨的:local cache,本地缓存,也叫进程缓存。

快速入门

作为一个进程存储设计,当然是 crud 都有的:

  1. 我们先初始化 local cache

// 先初始化 local cache
cache, err = collection.NewCache(time.Minute, collection.WithLimit(10))
if err != nil {
  log.Fatal(err)
}

其中参数的含义:

  • expire:key统一的过期时间

  • CacheOption:cache设置。比如key的上限设置等

  1. 基础操作缓存

// 1. add/update 增加/修改都是该API
cache.Set("first", "first element")

// 2. get 获取key下的value
value, ok := cache.Get("first")

// 3. del 删除一个key
cache.Del("first")
  • Set(key, value) 设置缓存

  • value, ok := Get(key) 读取缓存

  • Del(key) 删除缓存

  1. 高级操作

cache.Take("first", func() (interface{}, error) {
  // 模拟逻辑写入local cache
  time.Sleep(time.Millisecond * 100)
  return "first element", nil
})

前面的 Set(key, value) 是单纯将 加入缓存;Take(key, setFunc) 则是在 key 对于的 value 不存在时,执行传入的 fetch 方法,将具体读取逻辑交给开发者实现,并自动将结果放到缓存里。

到这里核心使用代码基本就讲完了,其实看起来还是挺简单的。也可以到 https://github.com/tal-tech/go-zero/blob/master/core/collection/cache_test.go 去看 test 中的使用。

解决方案

如何提高进程内缓存的并发

首先缓存实质是一个存储有限热点数据的介质,面临以下的这些问题:

  1. 有限容量

  2. 热点数据统计

  3. 多线程存取

下面来说说这3个方面我们的设计实践。

有限容量

有限就意味着满了要淘汰,这个就涉及到淘汰策略。cache 中使用的是:LRU(最近最少使用)。

那淘汰怎么发生呢? 有几个选择:

  1. 开一个定时器,不断循环所有key,等到了预设过期时间,执行回调函数(这里是删除map中过的key)

  2. 惰性删除。访问时判断该键是否被删除。缺点是:如果未访问的话,会加重空间浪费。

cache 中采取的是第一种 主动删除。但是,主动删除中遇到最大的问题是:

不断循环,空消耗CPU资源,即使在额外的协程中这么做,也是没有必要的。

cache 中采取的是时间轮记录额外过期通知,等过期 channel 中有通知时,然后触发删除回调。

> 有关 时间轮 更多的设计文章:https://go-zero.dev/cn/timing-wheel.html

热点数据统计

对于缓存来说,我们需要知道这个缓存在使用额外空间和代码的情况下是否有价值,以及我们想知道需不需要进一步优化过期时间或者缓存大小,所有这些我们就很依赖统计能力了, go-zerosqlcmongoc 也同样提供了统计能力。所以我们在 cache 中也加入的缓存,为开发者提供本地缓存监控的特性,在接入 ELK 时开发者可以更直观的监测到缓存的分布情况。

而设计其实也很简单,就是:Get() 命中,就在统计 count 上加1即可

func (c *Cache) Get(key string) (interface{}, bool) {
  value, ok := c.doGet(key)
  if ok {
    // 命中hit+1
    c.stats.IncrementHit()
  } else {
    // 未命中miss+1
    c.stats.IncrementMiss()
  }

  return value, ok
}

多线程存取

当多个协程并发存取的时候,对于缓存来说,涉及的问题以下几个:

  • 写-写冲突

  • LRU 中元素的移动过程冲突

  • 并发执行写入缓存时,造成流量冲击或者无效流量

这种情况下,写冲突好解决,最简单的方法就是 加锁

// Set(key, value)
func (c *Cache) Set(key string, value interface{}) {
  // 加锁,然后将  作为键值对写入 cache 中的 map
  c.lock.Lock()
  _, ok := c.data[key]
  c.data[key] = value
  // lru add key
  c.lruCache.add(key)
  c.lock.Unlock()
  ...
}

// 还有一个在操作 LRU 的地方时:Get()
func (c *Cache) doGet(key string) (interface{}, bool) {
  c.lock.Lock()
  defer c.lock.Unlock()
  // 当key存在时,则调整 LRU item 中的位置,这个过程也是加锁的
  value, ok := c.data[key]
  if ok {
    c.lruCache.add(key)
  }

  return value, ok
}

而并发执行写入逻辑,这个逻辑主要是开发者自己传入的。而这个过程:

func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
  // 1. 先获取 doGet() 中的值
  if val, ok := c.doGet(key); ok {
    c.stats.IncrementHit()
    return val, nil
  }

  var fresh bool
  // 2. 多协程中通过 sharedCalls 去获取,一个协程获取多个协程共享结果
  val, err := c.barrier.Do(key, func() (interface{}, error) {
    // double check,防止多次读取
    if val, ok := c.doGet(key); ok {
      return val, nil
    }
    ...
    // 重点是执行了传入的缓存设置函数
    val, err := fetch()
    ...
    c.Set(key, val)
  })
  if err != nil {
    return nil, err
  }
  ...
  return val, nil
}

sharedCalls 通过共享返回结果,节省了多次执行函数,减少了协程竞争。

以上就是如何提高进程内缓存的并发,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注编程笔记行业资讯频道。


推荐阅读
  • 面试经验分享:华为面试四轮电话面试、一轮笔试、一轮主管视频面试、一轮hr视频面试
    最近有朋友去华为面试,面试经历包括四轮电话面试、一轮笔试、一轮主管视频面试、一轮hr视频面试。80%的人都在第一轮电话面试中失败,因为缺乏基础知识。面试问题涉及 ... [详细]
  • 20211101CleverTap参与度和分析工具功能平台学习/实践
    1.应用场景主要用于学习CleverTap的使用,该平台主要用于客户保留与参与平台.为客户提供价值.这里接触到的原因,是目前公司用到该平台的服务~2.学习操作 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • yum安装_Redis —yum安装全过程
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Redis—yum安装全过程相关的知识,希望对你有一定的参考价值。访问https://redi ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • GPT-3发布,动动手指就能自动生成代码的神器来了!
    近日,OpenAI发布了最新的NLP模型GPT-3,该模型在GitHub趋势榜上名列前茅。GPT-3使用的数据集容量达到45TB,参数个数高达1750亿,训练好的模型需要700G的硬盘空间来存储。一位开发者根据GPT-3模型上线了一个名为debuid的网站,用户只需用英语描述需求,前端代码就能自动生成。这个神奇的功能让许多程序员感到惊讶。去年,OpenAI在与世界冠军OG战队的表演赛中展示了他们的强化学习模型,在限定条件下以2:0完胜人类冠军。 ... [详细]
  • 恶意软件分析的最佳编程语言及其应用
    本文介绍了学习恶意软件分析和逆向工程领域时最适合的编程语言,并重点讨论了Python的优点。Python是一种解释型、多用途的语言,具有可读性高、可快速开发、易于学习的特点。作者分享了在本地恶意软件分析中使用Python的经验,包括快速复制恶意软件组件以更好地理解其工作。此外,作者还提到了Python的跨平台优势,使得在不同操作系统上运行代码变得更加方便。 ... [详细]
  • 本文介绍了关系型数据库和NoSQL数据库的概念和特点,列举了主流的关系型数据库和NoSQL数据库,同时描述了它们在新闻、电商抢购信息和微博热点信息等场景中的应用。此外,还提供了MySQL配置文件的相关内容。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
author-avatar
mobiledu2502885111
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有