哈希表
哈希表是一种典型的以空间换取时间的数据结构,在没有冲突的情况下,对任意元素的插入、索引、删除的时间复杂度都是O(1)。
这样优秀的时间复杂度是通过将元素的key值以hash方法f映射到哈希表中的某一个位置来访问记录来实现的,即键值为key的元素
必定存储在哈希表中的f(key)的位置。当然,不同的元素的hash值可能相同,这就是hash冲突,有两种解决方法(分离链表发和开
放地址发),ngx采用的是开放地址法.
分离链表法是通过将冲突的元素链接在一个哈希表外的一个链表中,这样,找到hash表中的位置后,就可以通过遍历这个单链表来找到这个元素
开放地址法是插入的时候发现自己的位置f(key)已经被占了,就向后遍历,查看f(key)+1的位置是否被占用,如果没被占用,就占用它,
否则继续相后,查询的时候,同样也如果f(key)不是需要的值,也依次向后遍历,一直找到需要的元素。
哈希表的本质
普通哈希表的查找比较简单,思想就是先根据hash值找到对应桶,然后遍历这个桶的每一个元素,逐字匹配是否关键字完全相同,
完全相同则找到,否则继续,直至找到这个桶的结尾(value = NULL)。
nginx的hash表是固定元素长度的,就是一开始已知所有的键值对。无法动态添加,但是可以修改值
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#ifndef _NGX_HASH_H_INCLUDED_
#define _NGX_HASH_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
// 桶元素数据结构
typedef struct {
void *value; // 存放键值
u_short len; // 存放键长度
u_char name[1]; // 存放键名,这里是零长度数组的用法
} ngx_hash_elt_t;
typedef struct {
ngx_hash_elt_t **buckets; // 桶列表
ngx_uint_t size; // 桶个数
} ngx_hash_t; // 哈希表结构
// 键数据结构
typedef struct {
ngx_str_t key; // 存放键名
ngx_uint_t key_hash; // 存放键哈希值
void *value; // 存放键值
} ngx_hash_key_t;
typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);
typedef struct {
ngx_hash_t *hash; // 哈希表
ngx_hash_key_pt key; // 哈希算法
ngx_uint_t max_size; // 桶的个数最大值
ngx_uint_t bucket_size; // 桶的最大值,单位字节
char *name; // 哈希表名称
ngx_pool_t *pool; // 内存池
ngx_pool_t *temp_pool;
} ngx_hash_init_t;
void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);
ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);
#define ngx_hash(key, c) ((ngx_uint_t) key * 31 + c)
ngx_uint_t ngx_hash_key(u_char *data, size_t len);
ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len);
ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n);
#endif /* _NGX_HASH_H_INCLUDED_ */
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
void *
ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len)
{
ngx_uint_t i;
ngx_hash_elt_t *elt;
#if 0
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "hf:\"%*s\"", len, name);
#endif
// 通过哈希值找到对应的桶
elt = hash->buckets[key % hash->size];
if (elt == NULL) {
return NULL;
}
while (elt->value) {
if (len != (size_t) elt->len) {
goto next;
}
for (i = 0; i < len; i++) {
if (name[i] != elt->name[i]) {
goto next;
}
}
return elt->value;
next:
// 找到下一个元素的位置 &elt->name[0] + elt->len表示当前元素的末尾
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));
continue;
}
return NULL;
}
/*
设计说明:
这里是计算一个哈希表中桶中每个元素的大小
桶的元素格式如下
+---------------+---------------------+--------------------+
| 键值(void*) | 键名长度(u_short) | 键名(u_char []) |
+---------------+---------------------+--------------------+
因此单个桶元素的大小就是 键值+键名长度+键名
这里为了提升性能,使用了内存对齐 ngx_align
*/
#define NGX_HASH_ELT_SIZE(name) \
(sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
u_char *elts;
size_t len;
u_short *test;
ngx_uint_t i, n, key, size, start, bucket_size;
ngx_hash_elt_t *elt, **buckets;
// 桶的个数为0,这是不行的
if (hinit->max_size == 0) {
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
"could not build %s, you should "
"increase %s_max_size: %i",
hinit->name, hinit->name, hinit->max_size);
return NGX_ERROR;
}
// 单个桶的大小至少得能放置一个元素
for (n = 0; n < nelts; n++) {
// 遍历所有元素,保证桶的大小可以放进去每一个元素
/*
设计说明:
遍历所有元素,保证桶的大小可以放进去任何一个元素都是可以的
每个桶是以NULL结尾,因此是 NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *)
*/
if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
{
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
"could not build %s, you should "
"increase %s_bucket_size: %i",
hinit->name, hinit->name, hinit->bucket_size);
return NGX_ERROR;
}
}
// 分配一块临时内存空间,用来记录每个桶可以存放多个元素
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
if (test == NULL) {
return NGX_ERROR;
}
// 桶真实可以使用空间,需要将结束符NULL刨除掉
bucket_size = hinit->bucket_size - sizeof(void *);
/*
设计说明:
桶内元素的最小值就是 NGX_HASH_ELT_SIZE(),即2 * sizeof(void *)
(bucket_size / (2 * sizeof(void *))); 可以计算出每个桶至多可以存放的元素数量
nelts 是总元素数量
nelts / (bucket_size / (2 * sizeof(void *))) 即可以计算出桶个数的最小值
(bucket_size / (2 * sizeof(void *))); 这种算法是假设每个桶都塞满元素,现实中哈希表不可能每个桶都塞满元素
因此start已经算是桶个数的最小值了,真实值肯定比start大
*/
start = nelts / (bucket_size / (2 * sizeof(void *)));
start = start ? start : 1;
if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
// 当目前桶的数量足够大,那么start的起始节点就可以更大一些,减少不必要的遍历,算是一种用内存空间换性能的算法
start = hinit->max_size - 1000;
}
for (size = start; size <= hinit->max_size; size++) {
// 清理上一次选择的缓存
ngx_memzero(test, size * sizeof(u_short));
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {
// 键值为空就不用考虑了
continue;
}
// 通过哈希值来确认下标
key = names[n].key_hash % size;
// 当前下标又增加一个元素,因此桶的实际大小需要加上当前元素的大小
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: %ui %ui \"%V\"",
size, key, test[key], &names[n].key);
#endif
if (test[key] > (u_short) bucket_size) {
// 如果当前桶的大小已经超过限制,重新尝试下一种可能性
goto next;
}
}
// 遍历完所有元素,发现每个元素都可以放进当前哈希表中,并且桶的深度也算符合预期,那么最佳桶的个数就已经计算完成
goto found;
next:
continue;
}
size = hinit->max_size;
ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
"could not build optimal %s, you should increase "
"either %s_max_size: %i or %s_bucket_size: %i; "
"ignoring %s_bucket_size",
hinit->name, hinit->name, hinit->max_size,
hinit->name, hinit->bucket_size, hinit->name);
found:
// 此刻size就是最佳桶的个数
for (i = 0; i < size; i++) {
// 重置每个桶的大小
test[i] = sizeof(void *);
}
// 再次计算每个桶的实际大小
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {
continue;
}
key = names[n].key_hash % size;
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
}
len = 0;
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {
// 空桶略过
continue;
}
// 每个桶都要ngx_cacheline_size内存对齐,看出来nginx对于内存操作多么仔细
test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
len += test[i];
}
// 分配桶的内存
if (hinit->hash == NULL) {
hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
+ size * sizeof(ngx_hash_elt_t *));
if (hinit->hash == NULL) {
ngx_free(test);
return NGX_ERROR;
}
buckets = (ngx_hash_elt_t **)
((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
} else {
buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
if (buckets == NULL) {
ngx_free(test);
return NGX_ERROR;
}
}
// 分配桶内元素内存块
elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
if (elts == NULL) {
ngx_free(test);
return NGX_ERROR;
}
elts = ngx_align_ptr(elts, ngx_cacheline_size);
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {
continue;
}
// 根据计算好的桶元素,初始化每个桶元素内存空间
buckets[i] = (ngx_hash_elt_t *) elts;
elts += test[i];
}
for (i = 0; i < size; i++) {
test[i] = 0;
}
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {
continue;
}
// 为桶内每个元素赋值
key = names[n].key_hash % size;
elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
elt->value = names[n].value;
elt->len = (u_short) names[n].key.len;
ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
// 桶内位置偏移
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
}
// 为每个桶增加结束符NULL
for (i = 0; i < size; i++) {
if (buckets[i] == NULL) {
continue;
}
elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
/*
设计说明:
每个桶最后一个元素是NULL
这里有个特别的用法,单个桶的最后一个元素的大小是 sizeof(void*),实际上就是一个指针大小,虽然这里将这个内存空间强转成ngx_hash_elt_t*
实际上真正的内存空间却没有sizeof(ngx_hash_elt_t),只有value一个属性的大小,这里体现了nginx对内存细致入微的操作
*/
elt->value = NULL;
}
// 销毁临时内存
ngx_free(test);
hinit->hash->buckets = buckets;
hinit->hash->size = size;
#if 0
for (i = 0; i < size; i++) {
ngx_str_t val;
ngx_uint_t key;
elt = buckets[i];
if (elt == NULL) {
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: NULL", i);
continue;
}
while (elt->value) {
val.len = elt->len;
val.data = &elt->name[0];
key = hinit->key(val.data, val.len);
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: %p \"%V\" %ui", i, elt, &val, key);
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));
}
}
#endif
return NGX_OK;
}
ngx_uint_t
ngx_hash_key(u_char *data, size_t len)
{
ngx_uint_t i, key;
key = 0;
for (i = 0; i < len; i++) {
key = ngx_hash(key, data[i]);
}
return key;
}
ngx_uint_t
ngx_hash_key_lc(u_char *data, size_t len)
{
ngx_uint_t i, key;
key = 0;
for (i = 0; i < len; i++) {
key = ngx_hash(key, ngx_tolower(data[i]));
}
return key;
}
ngx_uint_t
ngx_hash_strlow(u_char *dst, u_char *src, size_t n)
{
ngx_uint_t key;
key = 0;
while (n--) {
*dst = ngx_tolower(*src);
key = ngx_hash(key, *dst);
dst++;
src++;
}
return key;
}