新建ets时默认配置为:
[set, protected, {keypos,1}, {heir,none}, {write_concurrency,false},
{read_concurrency,false}, {decentralized_counters,false}]
先看ets:new时会发生什么,即源码 erl_db.c 中 ets_new_2 函数核心实现为:
DbTable* tb = NULL; //ets表底层存储数据的结构
meth = &db_hash; //注, 设置ets为DB_ORDERED_SET类型时,meth 为 &db_catree
tb = (DbTable*) erts_db_alloc(ERTS_ALC_T_DB_TABLE, &init_tb, sizeof(DbTable));
//即 使用erl的 ERTS_ALC_T_DB_TABLE 内存分配器分配内存空间
ets查询逻辑源码为erl_db.c的ets_lookup_2
先根据ets表名或id标识获取tb,其核心逻辑为:
if (is_atom(id))
{
erts_rwmtx_t *mtl;
struct meta_name_tab_entry* bucket = meta_name_tab_bucket(id,&mtl);
//有一个全局的meta_name_tab数组,能根据id O(1) 的时间获取到tb结构
tb = NULL;
if (bucket->pu.tb != NULL) {
if (is_atom(bucket->u.name_atom)) {
if (bucket->u.name_atom == id)
tb = bucket->pu.tb;
}
}
}
else
tb = tid2tab(id); //同样是O(1)的时间消耗获取到tb
获取存储ets数据的tb结构后,执行查询操作
cret = tb->common.meth->db_get(BIF_P, tb, BIF_ARG_2, &ret);
其中meth为db_hash,可以在 erl_db_hash.c 文件中找到 db_get对应的实现逻辑为:
int db_get_hash(Process *p, DbTable *tbl, Eterm key, Eterm *ret)
{
DbTableHash *tb = &tbl->hash;
hval = MAKE_HASH(key); //对Key值进行hash运算
ix = hash_to_ix(tb, hval);//根据hash值获取索引
b = BUCKET(tb, ix);//根据索引从数据bucket中取值
while(b != 0) {
if (has_live_key(tb, b, key, hval)) { //遍历获取到的值,直到取到key对应的值
*ret = get_term_list(p, tb, key, hval, b, NULL);
goto done;
}
b = b->next;
}
*ret = NIL;
done:
RUNLOCK_HASH(lck);
return DB_ERROR_NONE;
}
ets插入逻辑源码为erl_db.c的ets_insert_2
其内容的核心为 erl_db_hash.c 文件中的db_put_hash:
int db_put_hash(DbTable *tbl, Eterm obj, int key_clash_fail, SWord *consumed_reds_p)
{
DbTableHash *tb = &tbl->hash;
int ret = DB_ERROR_NONE;
key = GETKEY(tb, tuple_val(obj));//获取key
hval = MAKE_HASH(key);//计算hash
ix = hash_to_ix(tb, hval);//根据hash获取索引
bp = &BUCKET(tb, ix);//获取该索引下的数据
b = *bp;
for (;;)
if (b == NULL) {//该索引下没有数据,则新建。否则遍历看有没有重复的key
goto Lnew;
}
if (has_key(tb,b,key,hval)) {
break;
}
bp = &b->next;
b = b->next;
}
if (tb->common.status & DB_SET) {
HashDbTerm* bnext = b->next;
q = replace_dbterm(tb, b, obj);//SET类型,替换重复key值
q->next = bnext;
*bp = q;
goto Ldone;
}
// else if bag类型的代码
Lnew:
q = new_dbterm(tb, obj); //该调用会申请obj占用的空间
q->hvalue = hval;
q->next = b;
*bp = q;
{
int nitems = &(tb)->common.counters;
int nactive = &(tb)->nactive;
if (nitems > GROW_LIMIT(nactive) && !IS_FIXED(tb)) {
grow(tb, nitems);//增长hash table
}
}
return DB_ERROR_NONE;
Ldone:
return ret;
}
综上我们可以发现ets(非DB_ORDERED_SET类)和进程字典都是基于hash算法进行keyvalue的存储。但是ets为了支持进程共享,所以有很多逻辑是用来进行锁的处理,以及存储时需要现申请keyvalue所用的空间;进程字典在进程堆区够用的情况下,是不需要额外申请空间的,空间不够时则会触发gc。