c++ 实现 key-value缓存数据结构

概述

最近在阅读Memcached的源代码,今天借鉴部分设计思想简单的实现了一个keyvalue缓存。

哈希表部分使用了unordered_map,用于实现LRU算法的双向链表嵌套在缓存类中实现。

LRU 算法

LRU算法又称为last least used 算法,是用于在缓冲区内存不足的情况下进行内存替换调度的算法,出于局部性原理,我们会将缓存中上一次使用时间最久远的元素删除,在这里我的实现算法如下:

将hash表中存储的数据地址(实现形式为存储数据类型的指针)用双向链表的形式存储,在一个元素被更新或者插入的时候会将该元素从链表中取出重新添加到链表头部,在LRU调度时,只需要将链表尾部的元素删除即可。

存储元素

对存储元素类的数据结构设计如下: Data作为粒度最小的数据单位存储,然而由于

template<typename K, typename V>
struct Data {
explicit Data(const K& k, const V& v) :key(k), val(v) { }
K key;
V val;
};

数据结构设计

  • 数据的存取基于哈希表来实现

为了照顾代码可读性,在这里使用了unordered_map。

链表节点实现粒度的考虑

  • 双向链表

    首先链表是通过包装Data形成一个双向链表节点实现。
  • 为什么不能使用std::list?

    在使用的粒度上std::list和此处的应用场景不同,考虑如下场合:通过get来查询哈希表中一个元素,此时由于这个元素被使用到了,应该从LRU链表中取出然后添加到链表头,如果使用std::list是难以实现的。因为它将list_node封装起来调用,我们无法通过哈希表中元素快速定位到链表中的迭代器位置。
  • 具体实现方式

    实现一个类似list_node节点来进行存储,链表在缓存中以头节点的形式存储。

    双线链表的实现 可参考:

    http://www.cnblogs.com/joeylee97/p/8549835.html

链表节点结构

  • 为什么通过指向const Data类型的shrared_ptr来存储数据?
  • Reason 1:

    Item之间的拷贝应该是轻量级的,这样能够提高存取性能
  • Reason 2:

    在高并发情况下,const Data的智能指针便于内存管理,而且可以减小锁的粒度。

    详细场景分析:在高并发情况下,此缓冲数据结构作为服务器端存储来使用,一个缓存区中数据应该怎样在读取时加锁?

    如果仅仅在取数据时期加锁,那么要做大量拷贝(从数据结构中拷贝到栈或者其他变量中),然后调用socket进行发送。

    如果我们在发送期间全程加锁,不仅效率极低,而且容易死锁。

    在这里我给出的方案是通过shared_ptr类型在读取时加锁,在发送时直接通过指针来读取数据内容(使用const Data)来避免线程之间读写冲突。

template<typename K, typename V>
struct Item {
typedef Item<K, V>* Itemptr;
typedef shared_ptr<const Data<K, V>> MData;
//for head node
Item(){} explicit Item(const K& k, const V& v) : nxt(nullptr), pre(nullptr) {
data = make_shared<Data<K, V>>(k, v);
}
//this should be a light weighted copy method since all its elems are ptr_type
Item(const Item& rhs) = default;
Item& operator=(const Item& rhs) = default;
//删除该元素
void detachFromList() {
Itemptr this_pre = pre, this_nxt = nxt;
this_pre->nxt = this_nxt;
this_nxt->pre = this_pre;
pre = nxt = nullptr; //In case this Item is reused
}
//加到该节点后面
void appendAftHead(Itemptr head) {
head->nxt->pre = this;
nxt = head->nxt;
head->nxt = this;
pre = head;
}
//for light copy and concurency
shared_ptr<const Data<K, V>> data;
Itemptr nxt;
Itemptr pre;
};

源码分析

哈希表接口

使用类应该通过模板偏特化来实现这两个接口

template<class T>
struct Hash {
size_t operator()(const T&) const;
}; template<class T>
struct Equal {
bool operator()(const T& lhs, const T& rhs) const;
};

Cache

template<class K, class V>
class Cache {
public:
typedef Item<K, V> MItem;
typedef shared_ptr<const Data<K, V>> MData;
typedef shared_ptr<const Data<K, V>> Dataptr;
typedef unordered_map<K, MItem, Hash<K>, Equal<K>> Table;
//对头节点初始化
explicit Cache(size_t capacity) :table(), head(), siz(0),cap(capacity) {
head.nxt = &head, head.pre = &head;
} //禁止拷贝
Cache(const Cache&) = delete;
Cache& operator=(const Cache&) = delete; std::pair<bool, Dataptr> get(const K& key) {
auto it = table.find(key);
if (it != table.end()) {
auto val = it->second.data->val;
it->second.detachFromList();
it->second.appendAftHead(&head);//调整到LRU首端
return { true, it->second.data };
}
else {
return { false, Dataptr() };
}
}
void put(const K& key, const V& val) {
auto it = table.find(key);
if (it != table.end()) {
it->second.detachFromList();
table.erase(it);
auto p = table.insert({ key, MItem(key, val) });
p.first->second.appendAftHead(&head);
}
else {
if (siz == cap) {
deleteLru();
}
auto p = table.insert({ key, MItem(key, val) }); //insert
p.first->second.appendAftHead(&head);
siz++;
}
} bool del(const K& key) {
auto it = table.find(key);
if (it == table.end()) {
return false;
}
else {
it->second.detachFromList();
table.erase(it);
siz--;
return true;
}
}
private:
//delete least recently used item
void deleteLru() {
MItem* lru = head.pre;
if (lru != &head) {
del(lru->data->key);
}
}
size_t cap;
size_t siz;
Table table;
MItem head;
};

设计的缺陷以及优化方向

首先Memcached 的数据结构是C语言定制的,所以在哈希表上性能会更突出,举个例子

void deleteLru() {
MItem* lru = head.pre;
if (lru != &head) {
del(lru->data->key);
}
}

在这个删除LRU链表尾部元素的操作过程中,我们由于不能从链表直接定位到哈希表,所以要有一个 o nlogn的查询操作,在定制化的数据结构中这个是O 1 的

set/map?

细心的读者会注意到,在hash_map中我们的key被存储了两次(一次在map_pair节点,一次在Item中),可以使用unordered_set 来存储Item,不过这样每次使用key都要进行一次类型组装(从key到Item),在时间上性能会下降,但是会节省空间。

c++ 实现 key-value缓存数据结构的更多相关文章

  1. ATS缓存数据结构

    ATS缓存数据结构 HttpTunnel类 数据传输驱动器(data transfer driver),包含一个生产者(producer)集合,每个生产者连接到一个或是多个消费者(comsumer). ...

  2. redis 一二事 - 设置过期时间,以文件夹形式展示key显示缓存数据

    在使用redis时,有时回存在大量数据的时候,而且分类相同,ID相同 可以使用hset来设置,这样有一个大类和一个小分类和一个value组成 但是hset不能设置过期时间 过期时间只能在set上设置 ...

  3. 第三节:Redis缓存雪崩、击穿、穿透、双写一致性、并发竞争、热点key重建优化、BigKey的优化 等解决方案

    一. 缓存雪崩 1. 含义 同一时刻,大量的缓存同时过期失效. 2. 产生原因和后果 (1). 原因:由于开发人员经验不足或失误,大量热点缓存设置了统一的过期时间. (2). 产生后果:恰逢秒杀高峰, ...

  4. 经典面试题:分布式缓存热点KEY问题如何解决--有赞方案

    有赞透明多级缓存解决方案(TMC) 一.引子 1-1. TMC 是什么 TMC ,即"透明多级缓存( Transparent Multilevel Cache )",是有赞 Paa ...

  5. (转载)遍历memcache中已缓存的key

    (转载)http://www.cnblogs.com/ainiaa/archive/2011/03/11/1981108.html 最近需要做一个缓存管理的功能.其中有一个需要模糊匹配memcache ...

  6. 缓存击穿、缓存失效及热点key的解决方案

    分布式缓存是网站服务端经常用到的一种技术,在读多写少的业务场景中,通过使用缓存可以有效地支撑高并发的访问量,对后端的数据库等数据源做到很好地保护.现在市面上有很多分布式缓存,比如Redis.Memca ...

  7. Redis缓存雪崩,缓存穿透,热点key解决方案和分析

    缓存穿透 缓存系统,按照KEY去查询VALUE,当KEY对应的VALUE一定不存在的时候并对KEY并发请求量很大的时候,就会对后端造成很大的压力. (查询一个必然不存在的数据.比如文章表,查询一个不存 ...

  8. Key/Value之王Memcached初探:二、Memcached在.Net中的基本操作

    一.Memcached ClientLib For .Net 首先,不得不说,许多语言都实现了连接Memcached的客户端,其中以Perl.PHP为主. 仅仅memcached网站上列出的语言就有: ...

  9. CRL快速开发框架系列教程五(使用缓存)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

随机推荐

  1. (转)IC设计完整流程及工具

    IC的设计过程可分为两个部分,分别为:前端设计(也称逻辑设计)和后端设计(也称物理设计),这两个部分并没有统一严格的界限,凡涉及到与工艺有关的设计可称为后端设计. 前端设计的主要流程: 1.规格制定 ...

  2. 磁盘格式化mke2fs

    -b 设置每个块的大小,当前支持1024,2048,40963种字节 -i 给一个inode多少容量 -c 检查磁盘错误,仅执行一次-c时候,会进行快速读取测试:-c -c会测试读写,会很慢 -L 后 ...

  3. redis源码分析之事务Transaction(下)

    接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...

  4. Android掌中游斗地主游戏源码完整版

    源码大放送-掌中游斗地主(完整版),集合了单机斗地主.网络斗地主.癞子斗地主等,有史以来最有参考价值的源码,虽然运行慢了一点但是功能正常,用的是纯java写的. 项目详细说明:http://andro ...

  5. (译文)IOS block编程指南 3 概念总览

    Conceptual Overview(概览) Block objects provide a way for you to create an ad hoc function body as an ...

  6. DROP TYPE - 删除一个用户定义数据类型

    SYNOPSIS DROP TYPE name [, ...] [ CASCADE | RESTRICT ] DESCRIPTION 描述 DROP TYPE 将从系统表里删除用户定义的类型. 只有类 ...

  7. slides 在线ppt && React && Angular

    现在主流前端框架 有3个 Vue React Angular 如果有时间就都学习,理解一下他们的差异性~ 在线ppt的一个网站 这个是npm讲解的,不错 https://slides.com/seld ...

  8. swift详解之十-------------异常处理、类型转换 ( Any and AnyObject )

    异常处理.类型转换 ( Any and AnyObject ) 1.错误处理 (异常处理) swift 提供第一类错误支持 ,包括在运行时抛出 ,捕获 , 传送和控制可回收错误.在swift中 ,错误 ...

  9. 洛谷 P1708 天然气井 题解

    https://www.luogu.org/problemnew/show/P1708 这道题还是比较好的. 读完题目我们先想想如何计算某个天然气井($x_1,y_1$)和中转站($a_1,b_1$) ...

  10. Django生成二维码

    1. 安装 pip install qrcode 安装Image包 pip install Image 1.1 在代码中使用 import qrcode img = qrcode.make('输入一个 ...