首先普及一下前置知识:
- 常用的数据库有3种,
- 关系型 如 Oracle,MySQL,Microsoft SQL Server,PostgreSQL
- 内存型 如 Redis, Memcached
- document 如 MongoDB
-
内存存储和磁盘存储的区别, 都知道内存比磁盘快, 那么具体快多少, 下面给出对比资料(大体的对比)
寻址时间:启动位置到达所要求的读/写位置所经历的全部时间。
带宽: 传输数据的时候数据流的速度- 寻址时间单位
- 普通硬盘 ms
- 固态硬盘 us
- 内存 ns
- 传输带宽
- 机械硬盘 100多Mb/s
- 固态硬盘 500Mb左右/s
- 主流ddr3 内存6Gb/s
- 存储方式
- 磁盘: 链式存储
- 内存:线性存储
- 寻址时间单位
- 为什么会出现缓存
- 世界上有全部使用内存的数据库引擎SAP HANA, 但是成本特别大
- 使用磁盘存储, 受限于io, 速度比较慢
- 即使使用全内存数据库, 也有网络IO的限制
- 使用缓存是一种折中的体现
- 如果能解决磁盘IO和网络IO的情况问题, 那可能改变现在计算机环境
PS:目前的计算机环境
- 冯诺依曼体系的硬件(未来是量子计算机)
- 以太网,tcp/ip的网络
redis基础整理
如图:
1. redis默认16个db
2. 每个db指向一个dist对象
3. 每个dict对象维护2个dictht, 第一个是使用的, 第二个是重构的
4. dictht 对象维护N个 dictEntry 对象
5. dictEntry 对象维护 redis的key和value
dictEntry 的*key 指向了 键值的key; *v 指向了键值的value, next指向了下一个 dictEntry
/* * 哈希表节点 */ typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next; } dictEntry;
dictht 采用 (MurmurHash)[http://calvin1978.blogcn.com/articles/murmur.html]和链表 维护了dictEntry,假设 dictht.table 创建了一个长度为4的dictEntry, 如图(演示了维护的过程):
/* * 哈希表 * * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。 */ typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht;
当数据越来越多,size只有4个,但是dictEntry 有几万个或者更多的时候,很显然上面那样子查询会变得很慢, 所以dict 就开始维护 dictht对象, 维护方式如图:
/* * 字典 */ typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */ } dict;
下面是一些默认的函数, 有兴趣可以看下
//initServer server.db[j].dict = dictCreate(&dbDictType,NULL); server.db[j].expires = dictCreate(&keyptrDictType,NULL); server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL); server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL); server.db[j].watched_keys = dictCreate(&keylistDictType,NULL); dictType dbDictType = { dictSdsHash, /* hash function */ NULL, /* key dup */ NULL, /* val dup */ dictSdsKeyCompare, /* key compare */ dictSdsDestructor, /* key destructor */ dictObjectDestructor /* val destructor */ }; dictType keyptrDictType = { dictSdsHash, /* hash function */ NULL, /* key dup */ NULL, /* val dup */ dictSdsKeyCompare, /* key compare */ NULL, /* key destructor */ NULL /* val destructor */ }; dictType keylistDictType = { dictObjHash, /* hash function */ NULL, /* key dup */ NULL, /* val dup */ dictObjKeyCompare, /* key compare */ dictObjectDestructor, /* key destructor */ dictListDestructor /* val destructor */ }; dictType objectKeyPointerValueDictType = { dictEncObjHash, /* hash function */ NULL, /* key dup */ NULL, /* val dup */ dictEncObjKeyCompare, /* key compare */ dictObjectDestructor, /* key destructor */ NULL /* val destructor */ };
上图只是简略的介绍了这个过程, 实际情况更复杂一些
- ht[0]=ht[1] 这个叫做 rehash
- rehash 的条件是 负载因子((ht[0].userd / ht[0].size) >= 1或者5) 或者 负载因子<0.1
- 服务器目前没有执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=1
- 服务器目前正在执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=5
- 扩展和收缩计算
- 若是扩展操作,那么ht[1]的大小为>=ht[0].used*2的2^n
- 若是收缩操作,那么ht[1]的大小为>=ht[0].used的2^n
- rehash 会带来额外的 io负载, 所以redis采用了渐进式rehash, 简单解释:就是先建立好 ht[1], 然后dictEntry的迁移是 在 对ht[0]的增删查改的过程中逐渐迁移到 ht[1]。
- 网 传 k e y 越 大 , 然 后 查 询 越 慢 , 其 实 从 上 面 的 结 构 就 可 以 看 出 来 , 这 句 话 要 这 么 理 解 \color{red} 网传key越大,然后查询越慢, 其实从上面的结构就可以看出来,这句话要这么理解 网传key越大,然后查询越慢,其实从上面的结构就可以看出来,这句话要这么理解
k e y 的 大 小 主 要 影 响 查 询 的 主 要 问 题 是 h a s h 的 计 算 , k e y 越 大 , h a s h 越 慢 , 得 到 h a s h 后 的 查 询 是 不 慢 的 \color{red} key的大小主要影响查询的主要问题是hash的计算,key越大,hash越慢,得到hash后的查询是不慢的 key的大小主要影响查询的主要问题是hash的计算,key越大,hash越慢,得到hash后的查询是不慢的- 想了解更多, 请看《Redis设计与实现(第二版)》
- 美团针对Redis Rehash机制的探索和实践