【项目日记(七)】第三层: 页缓存的具体实现(上)

简介: 【项目日记(七)】第三层: 页缓存的具体实现(上)

1. 前言

在页缓存这一层中,负责给中心缓存

分配大块儿的内存,以及合并前后空

闲的内存,这一层为整体加锁!

本章重点:

本篇文章着重讲解内存池第三层:
页缓存的基本成员变量和函数,以
页缓存的具体结构是怎样的.了解
完基础结构后,会详解讲解中心缓存
层来申请内存时的具体步骤!


2. 页缓存的具体结构

页缓存也是一个哈希桶结构,但它的映射

规则和前两层不同,它的规则是:

K号桶中的大块儿内存就是K页

并且它总共是有128号桶,申请小于

128页的内存都会在内存池中申请

//单例模式
class PageCache
{
public:
  static PageCache* GetInstance()
  {
    return &_singleton;
  }
  //获取一个K页的span
  SpanData* NewSpan(size_t k);
  std::mutex _mtx;//pagecache不能用桶锁,只能用全局锁,因为后面可能会有大页被切割为小页
  // 获取从对象到span的映射,给我一个地址,返回这个地址对应的span
  SpanData* MapObjectToSpan(void* obj);
  // 释放空闲span回到Pagecache,并合并相邻的span
  void ReleaseSpanToPageCache(SpanData* span);
private:
  PageCache(){}
  PageCache(const PageCache& obj) = delete;
private:
  std::unordered_map<PAGE_ID, SpanData*> _idSpanMap;//存储页号和桶中对应的span的映射,解决换回来的内存对应哪个span的问题
  SpanList _spanList[N_PAGES];
  static PageCache _singleton;
};

3. 页缓存分配内存的全过程

当中心缓存中没有内存时,会去页缓存
申请一个span结构,要经过下面几步:

  1. 根据中心缓存的桶号来确定申请的span是几页的
  2. 根据中心缓存想要申请的页数,找到页缓存中对应的桶(k页对应k号桶)
  3. 情况一: 页缓存的K号桶中存在span结构,直接将这块儿内存返回给中心缓存
  4. 情况二: 页缓存的K号桶没有span结构,但是K+1到128号桶中存在span结构,假设n号桶有span,则将这个大页的span切分为一个k页的span和一个n-k页的span,k页的span返回给中心缓存去使用,而将n-k页的span重新挂在n-k号桶中
  5. 情况三: k到128号桶都没有span,此时页缓存会向系统申请一份128页大小的内存,并挂在128号桶中,再将这个128页的span切分为k页的span和128-k页的span,也就转换为了情况二!

并且在这个过程中,页缓存将一个span
分配给中心缓存后,会记录下来这块儿
内存的页号和span的映射关系,方便后续
回收内存的时候再使用!


4. 页缓存分配内存的代码实现

在pagecache.h文件中:

SpanData* PageCache::NewSpan(size_t k)//去第K个桶中找span给central,此i号桶中挂的span都是i页内存
{
  //若K桶中有,直接返回,K桶没有span就往后找去分裂大span
  assert(k > 0);
  if (k > N_PAGES - 1)//如果申请的页数大于了128页,pagecache只能向堆申请了
  {
    void* ptr = SystemAlloc(k);
    SpanData* span = new SpanData();
    span->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
    span->_n = k;
    _idSpanMap[span->_pageid] = span;
    return span;
  }
  //先检查K号桶有无span,有直接返回一个
  if (!_spanList[k].Empty())
  {
    SpanData* KSpan = _spanList[k].PopFront();
    for (PAGE_ID i = 0; i < KSpan->_n; i++)
      _idSpanMap[KSpan->_pageid + i] = KSpan;
    return KSpan;
  }
  //走到这儿代表k号桶为空,检查后面的桶有没有span,拿出来分裂成两个小span
  for (int i = k + 1; i < N_PAGES; i++)
  {
    if (!_spanList[i].Empty())//k页的span返回给centralcache,i-k页的span挂到i-k号桶中
    {
      SpanData* ISpan = _spanList[i].PopFront();
      SpanData* KSpan = new SpanData;
      KSpan->_pageid = ISpan->_pageid;
      KSpan->_n = k;
      ISpan->_pageid += k;//把头K页切分给KSpan
      ISpan->_n -= k; //页数从i变为i-k
      _spanList[ISpan->_n].PushFront(ISpan);//再将后i-k页分配给i-k号桶
      //存储Ispan的首尾页号跟ISpan的映射关系
      // 这里只需要映射首尾页而不需要像下面一样全部页都映射,因为下面切分出去的span会被切分为小块儿内存
      // 这些小块儿内存都有可能被使用,所以当它们还回来时这些小块儿内存可能映射的是不同的页,但这些页都属于这个KSpan
      // 然而ISpan中不会被切分为小块儿内存,它只需要关心是否和它的前后页合并,所以这里只需要映射首尾页号与ISpan的关系
      // ISpan作为要合并页的前面,如1000页要合并ISpan是1001页,那么1001到1001+n都是空闲的!ISpan作为要合并页的后面,如100页要合并ISpan是999页,那么999-n都是空闲的!
      //_idSpanMap[ISpan->_pageid] = ISpan;
      //_idSpanMap[ISpan->_pageid + ISpan->_n - 1] = ISpan;
      _idSpanMap.set(ISpan->_pageid, ISpan);
      _idSpanMap.set(ISpan->_pageid + ISpan->_n - 1, ISpan);
      //建立id和span的映射关系,方便centralcache回收小块内存时查看哪块内存在哪块span
      for (PAGE_ID i = 0; i < KSpan->_n; i++)//返回的KSpan中一共有n页,并且每一页的页号都对应KSpan这个地址
        _idSpanMap[KSpan->_pageid + i] = KSpan;
      return KSpan;
    }
  }
  //走到这里说明后面所有的桶都没有span了
  //这时需要向堆申请一个128页的span再拿来做切分
  SpanData* bigSpan = _spanPool.New();
  void* ptr = SystemAlloc(N_PAGES - 1);
  bigSpan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
  bigSpan->_n = N_PAGES - 1;
  _spanList[bigSpan->_n].PushFront(bigSpan);//将这个128页的span插入到桶中
  return NewSpan(k);//再次调用自己,这次一定会在前面的for循环处返回
}

这个地方有一个设计的比较巧妙的点,
那就是出现情况三的时候,向系统申请了
128页的空间后,再次调用这个函数就一定
会出现情况二,从而在for循环中走完整个过程


5. 优化代码,并完全脱离malloc

细心的同学会发现,在这个函数中使用到了new操作符,然而了解new底层原理的同学应该知道,new的底层实际上是用的malloc来申请的空间,但是我们这个项目就是为了完全脱离malloc函数来实现一个多线程下高效的内存池,所以这里一定不能使用new!

使用之前编写的定长池来舍弃new!

如果你不知道或忘记了定长池是什么

请看这篇文章: 定长池的实现

首先, 在页缓存类中添加上一个成员变量: 定长池类, 然后在使用new的地方,把new全部替换为用定长池申请空间!


6. 总结以及代码拓展

页缓存分配内存的一环设计的是

非常的巧妙,但是页缓存真正巧妙

的地方是在合并空闲内存的一环!

对代码的拓展:

我们会发现页缓存结构中调用了

好几次向系统申请内存的函数,

这个地方只做了解,会用接口就行

inline static void* SystemAlloc(size_t kpage)//申请kpage页内存
{
#ifdef _WIN32
  void* ptr = VirtualAlloc(0, kpage << PAGE_SHIFT, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
  // linux下brk mmap等直接向系统申请内存的方式
#endif
  if (ptr == nullptr)
    throw std::bad_alloc();
  return ptr;
}

🔎 下期预告:页缓存的具体实现(下)🔍


相关文章
|
1月前
|
缓存 开发框架 移动开发
uni-app:下载使用uni&创建项目&和小程序链接&数据缓存&小程序打包 (一)
uni-app 是一个跨平台的开发框架,它允许开发者使用 Vue.js 来构建应用程序,并能够同时发布到多个平台,如微信小程序、支付宝小程序、H5、App(通过DCloud的打包服务)等。uni-app 的目标是通过统一的代码库,简化多平台开发过程,提高开发效率。 在这一部分中,我们将逐步介绍如何下载和使用uni-app、创建一个新的项目、如何将项目链接到小程序,以及实现数据缓存的基本方法。
|
2月前
|
缓存 NoSQL Java
瑞吉外卖项目笔记+踩坑2——缓存、读写分离优化
缓存菜品、套餐数据、mysql主从复制实现读写分离、前后端分离
瑞吉外卖项目笔记+踩坑2——缓存、读写分离优化
|
3月前
|
缓存 开发框架 .NET
看看 Asp.net core Webapi 项目如何优雅地使用内存缓存
看看 Asp.net core Webapi 项目如何优雅地使用内存缓存
|
3月前
|
存储 缓存 开发框架
看看 Asp.net core Webapi 项目如何优雅地使用分布式缓存
看看 Asp.net core Webapi 项目如何优雅地使用分布式缓存
|
5月前
|
敏捷开发 缓存 测试技术
阿里云云效产品使用问题之构建Vue3项目,怎么让node_modules缓存下来
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
6月前
|
缓存
【项目日记(八)】第三层: 页缓存的具体实现(下)
【项目日记(八)】第三层: 页缓存的具体实现(下)
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
7天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
8天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构