c++ 实现 key-value缓存数据结构
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缓存数据结构的更多相关文章
- ATS缓存数据结构
ATS缓存数据结构 HttpTunnel类 数据传输驱动器(data transfer driver),包含一个生产者(producer)集合,每个生产者连接到一个或是多个消费者(comsumer). ...
- redis 一二事 - 设置过期时间,以文件夹形式展示key显示缓存数据
在使用redis时,有时回存在大量数据的时候,而且分类相同,ID相同 可以使用hset来设置,这样有一个大类和一个小分类和一个value组成 但是hset不能设置过期时间 过期时间只能在set上设置 ...
- 第三节:Redis缓存雪崩、击穿、穿透、双写一致性、并发竞争、热点key重建优化、BigKey的优化 等解决方案
一. 缓存雪崩 1. 含义 同一时刻,大量的缓存同时过期失效. 2. 产生原因和后果 (1). 原因:由于开发人员经验不足或失误,大量热点缓存设置了统一的过期时间. (2). 产生后果:恰逢秒杀高峰, ...
- 经典面试题:分布式缓存热点KEY问题如何解决--有赞方案
有赞透明多级缓存解决方案(TMC) 一.引子 1-1. TMC 是什么 TMC ,即"透明多级缓存( Transparent Multilevel Cache )",是有赞 Paa ...
- (转载)遍历memcache中已缓存的key
(转载)http://www.cnblogs.com/ainiaa/archive/2011/03/11/1981108.html 最近需要做一个缓存管理的功能.其中有一个需要模糊匹配memcache ...
- 缓存击穿、缓存失效及热点key的解决方案
分布式缓存是网站服务端经常用到的一种技术,在读多写少的业务场景中,通过使用缓存可以有效地支撑高并发的访问量,对后端的数据库等数据源做到很好地保护.现在市面上有很多分布式缓存,比如Redis.Memca ...
- Redis缓存雪崩,缓存穿透,热点key解决方案和分析
缓存穿透 缓存系统,按照KEY去查询VALUE,当KEY对应的VALUE一定不存在的时候并对KEY并发请求量很大的时候,就会对后端造成很大的压力. (查询一个必然不存在的数据.比如文章表,查询一个不存 ...
- Key/Value之王Memcached初探:二、Memcached在.Net中的基本操作
一.Memcached ClientLib For .Net 首先,不得不说,许多语言都实现了连接Memcached的客户端,其中以Perl.PHP为主. 仅仅memcached网站上列出的语言就有: ...
- CRL快速开发框架系列教程五(使用缓存)
本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...
随机推荐
- Git使用简析
推送本地操作 初始化一个本地Git仓库,在需要添加版本控制的文件夹根目录中使用git init命令. 添加文件到本地Git仓库: git add 文件名 # 添加文件到暂存区 git add . # ...
- Javaweb学习笔记8—DBUtils工具包
今天来讲javaweb的第8阶段学习. DBUtils技术,DBUtils是我们操作数据库很常用的功能,虽然后期使用都是它的封装结果,但是也需要掌握. 老规矩,首先先用一张思维导图来展现今天的博客内容 ...
- 【HEVC简介】ALF-Adative Loop Filter
由于HEVC在HM4.0之后,就把ALF去掉,所以ALF的介绍是基于AVS2. <HEVC标准介绍.HEVC帧间预测论文笔记>系列博客,目录见:http://www.cnblogs.com ...
- ubuntu4.04服务器添加虚拟主机
buntu 14.04配置虚拟主机 虚拟主机常用于在一个单独的IP地址上提供多个域名的网站服务.如果有人想在单个VPS的单个IP地址运行多个网站,这是非常有用的.在这个教程中,让我告诉你如何设置在 ...
- Makeflie自动生成依赖,自动化编译
在netbeans里开发,有一个重要文件makefile,是用来编译所有的文件. 项目的目录结构如下,扁平的目录结构,如何实现自动化编译,写makefile呢? 第一版 基础版: CC = g++ C ...
- SQLite与MySQL、SQLServer等异构数据库之间的数据同步
SQLite DBSync是开源嵌入式数据库SQLite的数据同步引擎,实现了SQLite与SQLite数据库之间以及SQLite与异构数据库(Oracle.MySQL.SQLServer)之间的增量 ...
- jmeter 接口测试简介
前言: 本文主要针对http接口进行测试,使用Jmeter工具实现. Jmter工具设计之初是用于做性能测试的,它在实现对各种接口的调用方面已经做的比较成熟,因此,本次直接使用Jmeter工具来完成对 ...
- uva1611 Crane
类似煎饼,先把1放到1,之后是子问题 (先放到前一半,再放到开头,两次操作)(任何位置,最多一次就可以放到前一半)) #include<iostream> #include<ve ...
- Java中类,对象,方法的内存分配
Java中类,对象,方法的内存分配 以下针对引用数据类型: 在内存中,类是静态的概念,它存在于内存中的CodeSegment中. 当我们使用new关键字生成对象时,JVM根据类的代码,去堆内存中开辟一 ...
- vue props 下有验证器 validator 验证数据返回true false后,false给default值
vue props 下有验证器 validator 验证数据返回true false后,false给default值 props: { type: { validator (value) { retu ...