Redis 原理及实战(一)
Redis是什么
Redis是一个单线程nosql数据库。所有操作都是原子性
一.场景
生产者消费者,缓存,秒杀场景时缓冲。点赞计数,session同步,分布式锁。
二.基本数据类型
Redis五种数据类型
1.String
string:redis String底层的数据结构时SDS
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
记录了长度,也就是每次增加的时候只比较一次不用重写查询一遍。
内存分配策略为预分配。
2.List
list是一个链表
typedef struct list{
//表头节点
listNode * head;
//表尾节点
listNode * tail;
//链表长度
unsigned long len;
//节点值复制函数
void *(*dup) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match)(void *ptr, void *key);
}
3.Hash
解决hash冲突:采用链地址法来实现。
扩充Rehash: 随着对哈希表的不断操作,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成。其实现方式和hashmap略有不同,因为dict有两个hash表dictht,所以它是通过这两个dictht互相进行转移的。
渐进式rehash:在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。
渐进式rehash 的详细步骤:
1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始
3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一
4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束
4.set
底层也是数组。
5.sortSet
跳表
怎么使用跳表来实现O(logn)的增删改查??
其实跳表的实现原理,我们可以结合二分法来看。
L1 -inf->3->2->17->21->33->37->46->55
比如上图,我们要查找55,如果通过遍历,则必须得从头遍历到最后一个才能找到,所以在数组实现中,我们可以使用二分法来实现,但是在链表中,我们没办法直接通过下标来访问元素,所以一般我们可以用二叉搜索树,平衡树来存储元素,我们知道跳表就是来替代平衡树的,那么跳表是如何快速查询呢?看下图:
-inf———————————————55
-inf——————>21———————-55
-inf——>2———>21——>37———>55
-inf->-3->2->17->21->33->37->46->55
从上图我们可以看到,我们通过第4层,只需一步便可找到55,另外最耗时的访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。我们直觉上认为,这样的结构会让查询有序链表的某个元素更快。这种实现方式跟二分很相似,其时间复杂度就是O(logn)。其插入,删除都是O(logn)。
我们可以看到,redis正是通过定义这种结构来实现上边的过程,其层数最高为32层,也就是他可以存储2^32次方的数据,其查找过程与上图很类似。
三.Rehash
Redis Rehash 内部实现
在Redis中,键值对(Key-Value Pair)存储方式是由字典(Dict)保存的,而字典底层是通过哈希表来实现的。通过哈希表中的节点保存字典中的键值对。类似Java中的HashMap,将Key通过哈希函数映射到哈希表节点位置。
接下来我们一步步来分析Redis Dict Reash的机制和过程。
Redis 哈希表中的table数组存放着哈希桶结构(dictEntry),里面就是Redis的键值对;类似Java实现的HashMap,Redis的dictEntry也是通过链表(next指针)方式来解决hash冲突:
/* 哈希桶 */
typedef struct dictEntry {
void *key; // 键定义
// 值定义
union {
void *val; // 自定义类型
uint64_t u64; // 无符号整形
int64_t s64; // 有符号整形
double d; // 浮点型
} v;
struct dictEntry *next; //指向下一个哈希表节点
} dictEntry;
Redis Dict 中定义了两张哈希表,是为了后续字典的扩展作Rehash之用:
/* 字典结构定义 */
typedef struct dict {
dictType *type; // 字典类型
void *privdata; // 私有数据
dictht ht[2]; // 哈希表[两个]
long rehashidx; // 记录rehash 进度的标志,值为-1表示rehash未进行
int iterators; // 当前正在迭代的迭代器数
} dict;
总结一下:
在Cluster模式下,一个Redis实例对应一个RedisDB(db0);
一个RedisDB对应一个Dict;
一个Dict对应2个Dictht,正常情况只用到ht[0];ht[1] 在Rehash时使用。
* 根据相关触发条件扩展字典 */
static int _dictExpandIfNeeded(dict *d)
{
if (dictIsRehashing(d)) return DICT_OK; // 如果正在进行Rehash,则直接返回
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); // 如果ht[0]字典为空,则创建并初始化ht[0]
/* (ht[0].used/ht[0].size)>=1前提下,
当满足dict_can_resize=1或ht[0].used/t[0].size>5时,便对字典进行扩展 */
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2); // 扩展字典为原来的2倍
}
return DICT_OK;
}
...
/* 计算存储Key的bucket的位置 */
static int _dictKeyIndex(dict *d, const void *key)
{
unsigned int h, idx, table;
dictEntry *he;
/* 检查是否需要扩展哈希表,不足则扩展 */
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
/* 计算Key的哈希值 */
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask; //计算Key的bucket位置
/* 检查节点上是否存在新增的Key */
he = d->ht[table].table[idx];
/* 在节点链表检查 */
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return -1;
he = he->next;
}
if (!dictIsRehashing(d)) break; // 扫完ht[0]后,如果哈希表不在rehashing,则无需再扫ht[1]
}
return idx;
}
...
/* 将Key插入哈希表 */
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
if (dictIsRehashing(d)) _dictRehashStep(d); // 如果哈希表在rehashing,则执行单步rehash
/* 调用_dictKeyIndex() 检查键是否存在,如果存在则返回NULL */
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(sizeof(*entry)); // 为新增的节点分配内存
entry->next = ht->table[index]; // 将节点插入链表表头
ht->table[index] = entry; // 更新节点和桶信息
ht->used++; // 更新ht
/* 设置新节点的键 */
dictSetKey(d, entry, key);
return entry;
}
...
/* 添加新键值对 */
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key); // 添加新键
if (!entry) return DICT_ERR; // 如果键存在,则返回失败
dictSetVal(d, entry, val); // 键不存在,则设置节点值
return DICT_OK;
}
案例-Redis 满容状态下由于Rehash导致大量Key驱逐
在Redis Rehash源码实现的逻辑上,加上了一个判断条件,如果现有的剩余内存不够触发Rehash操作所需申请的内存大小,即不进行Resize操作;
通过提前运营进行规避,比如容量预估时将Rehash占用的内存考虑在内,或者通过监控定时扩容。