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. iOS 随笔小技巧 弱self 打印当前类行数列数,多人开发自动适配pch地址,获取设备uid的信息

    $(SRCROOT)/PrefixHeader.pch自动适配pch地址 __weak __block typeof(self) weakself = self; __weak typeof(self ...

  2. Keil简介

    最早接触Keil是学习开发8051系列的单片机.Keil C51是Keil公司出品的51系列兼容单片机C语言软件开发系统,与汇编相比,C语言在功能上.结构性.可读性.可维护性上有明显的优势,因而易学易 ...

  3. php 批量依照ID建立 文件

    <?php // 登录验证 include_once('inc/conn.php'); // sql查询 $sql="SELECT * FROM zcgl ";// $res ...

  4. 【C++】双边滤波器(bilateral filter)

    Bilateral Filtering for Gray and Color Images 双边滤波器:保留边界的平滑滤波器. 在局部上,就是在灰度值差异不大的区域平滑,在灰度值差异比较大的边界地区保 ...

  5. SQLite-Like语句

    SQLite – LIKE子句 使用SQLite LIKE运算符 用于匹配文本.如果搜索表达式可以匹配模式表达式,如操作符将返回true,这是1.有两个通配符与Like操作符一起使用: The per ...

  6. 7-Java-C(四平方和)

    题目描述: 四平方和定理,又称为拉格朗日定理: 每个正整数都可以表示为至多4个正整数的平方和. 如果把0包括进去,就正好可以表示为4个数的平方和. 比如: 5 = 0^2 + 0^2 + 1^2 + ...

  7. iview table的render()函数基本的用法

    render:(h,params) => { return h(" 定义的元素 ",{ 元素的性质 }," 元素的内容"/[元素的内容]) }

  8. 数据库sql语句limit区别

    注意:并非所有的数据库系统都支持 SELECT TOP 语句. MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取. SQL Server / MS ...

  9. OpenCV2:第六章 图像几何变换

    一.简介 图像的几何变换有距离变换 坐标映射 平移  镜像 旋转  缩放  仿射变换等 二.重映射 把一张图像重新排列像素,比如倒置 CV_EXPORTS_W void remap( InputArr ...

  10. Java中的类加载器--Class loader

    学习一下Java中的类加载器,这个是比较底层的东西,好好学习.理解一下.  一.类加载器的介绍 1.类加载器:就是加载类的工具,在java程序中用到一个类,java虚拟机首先要把这个类的字节码加载到内 ...