用文件的方式读写,一个文件是索引文件,另一个文件是真实的数据文件。
索引文件分为2部分,第一部分是所有的指针,记录第二部分的位置;第二部分是索引记录。所有的索引指针:是记录所有相同Hash值的key的指针,它是一个链表结构,记录在数据文件的位置和同key的下一个值。
索引记录中:每条记录有四部分,第一部分4个字节,是下一条索引的偏移量;第二部分是该记录的key,128字节;第三部分是数据偏移量,4个字节;第四部分是数据记录长度,4个字节。
我们设定文件的存储上限为262144个。
查找流程如下:
1、根据key算出hash值,获取该hash值的链表在索引文件的第一部分(所有指针区)的位置。
2、根据步骤一的位置,获取值,时间复杂度O(1);
2、根据步骤一中的值,找到索引文件中第二部分(索引记录)的位置,也就是和key相同hash值的所有指针的链表。顺着链表查找该key,获取该key在链表中存放的数据,数据只包含该key在索引文件中的位置,时间复杂度为O(n);
3、根据步骤二所获取的key在索引文件位置,得到索引文件中存放该key的信息。信息包含在真实数据文件中存放真实数据的位置。
4、根据步骤三所获取的位置,在真实数据文件中获取数据,并返回给应用程序。
测试结果:插入10000条耗时:793ms。查找10000条耗时:149ms。虽然这效率只有Redis的十分之一。。。但是请不要在意这些细节。。。
代码做了注释,上述文字有些乱。代码只实现三个方法,一个插入(如果存在则跳过),一个是查找,一个是删除。
<?php
//Hash表中的元素指针个数,每个指针都是int,存储hash链表的文件偏移量
define(\'DB_BUCKET_SIZE\', 262144);
//每条记录的key的长度
define(\'DB_KEY_SIZE\', 128);
//一条索引记录的长度
define(\'DB_INDEX_SIZE\', DB_KEY_SIZE + 12);
//成功-返回码
define(\'DB_SUCCESS\', 1);
//失败-返回码
define(\'DB_FAILURE\', -1);
//key重复-返回码
define(\'DB_KEY_EXISTS\', -2);
class DB{
private $idx_fp;
private $dat_fp;
private $closed;
/**
* De ion: 打开数据库
* @param $pathName 数据文件的存放路径
* @return mixed
*/
public function open($pathName){
$idx_path = $pathName . \'.idx\';
$dat_path = $pathName . \'.dat\';
if(!file_exists($idx_path)){
$init = true;
$mode = \"w+b\";
}else{
$init = false;
$mode = \'r+b\';
}
$this->idx_fp = fopen($idx_path, $mode);
if(!$this->idx_fp){
return DB_FAILURE;
}
if($init){
//把0x00000000转换成无符号长整型的二进制
$elem = pack(\'L\', 0x00000000);
for($i=0; $i< DB_BUCKET_SIZE; $i++){
fwrite($this->idx_fp, $elem, 4);
}
}
$this->dat_fp = fopen($dat_path, $mode);
if(!$this->dat_fp){
return DB_FAILURE;
}
return DB_SUCCESS;
}
/**
* De ion: Times33 Hash算法
* @param $key
* @return int
*/
private function times33Hash($key){
$len = 8;
$key = substr(md5($key), 0, $len);
$hash = 0;
for($i=0; $i<$len; $i++){
$hash += 33 * $hash + ord($key[$i]);
}
//0x7FFFFFFF:一个十六进制的数是4bit,8个就是32位,就是4字节,和一个int一样大。而F是1111,7是0111,那么这个十六进制的数就是头为0,其余为1的,首位是符号位,也就是说7fffffff是最大的整数。
//& 0x7fffffff 可以保证返回的数是正整数
return $hash & 0x7FFFFFFF;
}
/**
* De ion: 插入记录
* @param $key
* @param $value
*/
public function add($key, $value){
$offset = ($this->times33Hash($key) % DB_BUCKET_SIZE) * 4;
$idxoff = fstat($this->idx_fp);
$idxoff = intval($idxoff[\'size\']);
$datoff = fstat($this->dat_fp);
$datoff = intval($datoff[\'size\']);
$keylen = strlen($key);
$vallen = strlen($value);
if($keylen > DB_KEY_SIZE){
return DB_FAILURE;
}
//0表示这是最后一个记录,该链再无其他记录。
$block = pack(\'L\', 0x00000000);
//键值
$block .= $key;
//如果键值的长度没有达到最大长度,则用0填充
$space = DB_KEY_SIZE - $keylen;
for($i=0; $i<$space; $i++){
$block .= pack(\'C\', 0x00);
}
//数据所在文件的偏移量
$block .= pack(\'L\', $datoff);
//数据记录的长度
$block .= pack(\'L\', $vallen);
//尽管SEEK_SET是默认值,但是显式声明了就不怕以后官方会改变了-.-
fseek($this->idx_fp, $offset, SEEK_SET);
//检测该key所对应的hash值是否存在了
$pos = @unpack(\'L\', fread($this->idx_fp, 4));
$pos = $pos[1];
//如果key不存在
if($pos == 0){
fseek($this->idx_fp, $offset, SEEK_SET);
fwrite($this->idx_fp, pack(\'L\', $idxoff), 4);
fseek($this->idx_fp, 0, SEEK_END);
fwrite($this->idx_fp, $block, DB_INDEX_SIZE);
fseek($this->dat_fp, 0, SEEK_END);
fwrite($this->dat_fp, $value, $vallen);
return DB_SUCCESS;
}
//如果key存在
$found = false;
while($pos){
fseek($this->idx_fp, $pos, SEEK_SET);
$tmp_block = fread($this->idx_fp, DB_INDEX_SIZE);
$cpkey = substr($tmp_block, 4, DB_KEY_SIZE);
//$cpkey==$key时返回0,小于返回负数,大于返回正数
if(!strncmp($cpkey, $key, $keylen)){
$dataoff = unpack(\'L\', substr($tmp_block, DB_KEY_SIZE + 4, 4));
$dataoff = $dataoff[1];
$datalen = unpack(\'L\', substr($tmp_block, DB_KEY_SIZE + 8, 4));
$datalen = $datalen[1];
$found = true;
break;
}
$prev = $pos;
$pos = @unpack(\'L\', substr($tmp_block, 0, 4));
$pos = $pos[1];
}
if($found){
return DB_KEY_EXISTS;
}
fseek($this->idx_fp, $prev, SEEK_SET);
fwrite($this->idx_fp, pack(\'L\', $idxoff), 4);
fseek($this->idx_fp, 0, SEEK_END);
fwrite($this->idx_fp, $block, DB_INDEX_SIZE);
fseek($this->dat_fp, 0, SEEK_END);
fwrite($this->dat_fp, $value, $vallen);
return DB_SUCCESS;
}
/**
* De ion: 查询一条记录
* @param $key
*/
public function get($key){
//计算偏移量,key的hash值对索引文件的大小求模,再乘4。因为每个链表指针大小为4
$offset = ($this->times33Hash($key) % DB_BUCKET_SIZE) * 4;
//SEEK_SET是默认的
fseek($this->idx_fp, $offset, SEEK_SET);
$pos = unpack(\'L\', fread($this->idx_fp, 4));
$pos = $pos[1];
$found = false;
while($pos){
fseek($this->idx_fp, $pos, SEEK_SET);
$block = fread($this->idx_fp, DB_INDEX_SIZE);
$cpkey = substr($block, 4, DB_KEY_SIZE);
if(!strncmp($key, $cpkey, strlen($key))){
$dataoff = unpack(\'L\', substr($block, DB_KEY_SIZE + 4, 4));
$dataoff = $dataoff[1];
$datalen = unpack(\'L\', substr($block, DB_KEY_SIZE + 8, 4));
$datalen = $datalen[1];
$found = true;
break;
}
$pos = unpack(\'L\', substr($block, 0, 4));
$pos = $pos[1];
}
if(!$found){
return null;
}
fseek($this->dat_fp, $dataoff, SEEK_SET);
$data = fread($this->dat_fp, $datalen);
return $data;
}
/**
* De ion: 删除
* @param $key
*/
public function delete($key){
$offset = ($this->times33Hash($key) % DB_BUCKET_SIZE) * 4;
fseek($this->idx_fp, $offset, SEEK_SET);
$head = unpack(\'L\', fread($this->idx_fp, 4));
$head = $head[1];
$curr = $head;
$prev = 0;
$found = false;
while($curr){
fseek($this->idx_fp, $curr, SEEK_SET);
$block = fread($this->idx_fp, DB_INDEX_SIZE);
$next = unpack(\'L\', substr($block, 0, 4));
$next = $next[1];
$cpkey = substr($block, 4, DB_KEY_SIZE);
if(!strncmp($key, $cpkey, strlen($key))){
$found = true;
break;
}
$prev = $curr;
$curr = $next;
}
if(!$found){
return DB_FAILURE;
}
//删除索引文件。
if($prev == 0){
fseek($this->idx_fp, $offset, SEEK_SET);
fwrite($this->idx_fp, pack(\'L\', $next), 4);
}else{
fseek($this->idx_fp, $prev, SEEK_SET);
fwrite($this->idx_fp, pack(\'L\', $next), 4);
}
return DB_SUCCESS;
}
public function close(){
if(!$this->closed){
fclose($this->idx_fp);
fclose($this->dat_fp);
$this->closed = true;
}
}
}
?>
测试,测试添加一万条和查找一万条:
<?php
//先include上面的类。。如果在同一个文件中就不用了。
//测试
$db = new DB();
$db->open(\'/var/www/data/\');
$startTime = microtime(true);
//插入测试...插入10000条:成功,耗时: 793.48206520081ms
//for($i=0; $i<10000; $i++){
// $db->add(\'key\'.$i, \'value\'.$i);
//}
//查找测试...查找10000条:成功,耗时: 149.08313751221ms
for($i=0; $i<10000; $i++){
$db->get(\'key\'.$i);
}
$endTime = microtime(true);
echo \'成功,耗时: \' . (($endTime - $startTime)*1000) . \'ms\';
$db->close();
?>
继续阅读与本文标签相同的文章
-
超进化,时尚玩主的全新一代名爵ZS,起步只要7.98万
2026-05-14栏目: 教程
-
微信 7.0.8 内测新功能,教你如何激活它
2026-05-14栏目: 教程
-
美女机器人刚上市就售罄,除了生孩子,其他什么都能干!
2026-05-14栏目: 教程
-
互联网之光大会的黑科技,总有一款惊艳你!
2026-05-14栏目: 教程
-
微信宣布一项新举措,关系到每一个用户,网友一致力挺:干得漂亮!
2026-05-14栏目: 教程
