eftec/documentstoreone

一个支持多并发的PHP扁平文档存储

1.27 2024-07-19 15:22 UTC

This package is auto-updated.

Last update: 2024-09-19 15:45:11 UTC


README

一个支持多并发的PHP文档存储。它是一个MongoDB或CouchDB的轻量级替代品,无需安装新服务。

它还可以作为一个小型的数据库使用。

Packagist Total Downloads License Maintenance composer php php php Doc

关键特性

  • 基于单个键。
  • 速度快。然而,它不是关系型数据库的替代品。它优化了存储一定数量的文档而不是数百万行。
  • 通过锁定和解锁文档来支持多并发。如果文档被锁定,则重试直到文档解锁或重试次数达到限制。
  • 一个没有依赖的单个类。
  • 自动解锁被锁定的文档(默认情况下,如果文件被锁定,则每2分钟解锁一次)。
  • 它可以使用 MapReduce 请参阅 示例

测试

平均而言,一个小型企业每月生成100张发票。所以,假设一个小型企业每十年生成12,000张发票。

在一个i7/ssd/16gb/windows 64位系统上测试生成12,000张发票,包括客户信息、详细信息(每项详情约1-5行)和日期。

  • 存储12,000张发票用时45.303秒(预留序列号范围)
  • 存储12,000张发票用时73.203秒(每次读取序列号)
  • 存储12,000张发票用时49.0286秒(预留序列号范围并使用igbinary)
  • 读取所有发票用时60.2332秒。(仅读取)
  • 按客户对所有发票进行MapReduce操作用时64.0569秒。
  • 按客户对所有发票进行MapReduce操作用时32.9869秒(igbinary)
  • 从客户读取所有发票用时 0.3秒。(包括渲染结果,见图像)
  • 添加新的发票而无需重新计算所有MapReduce操作用时0.011秒。

mapreduce example

并发测试

进行了100个并发测试(写入和读取),10次。

使用方法

include "lib/DocumentStoreOne.php";
use eftec\DocumentStoreOne\DocumentStoreOne;
try {
    $flatcon = new DocumentStoreOne("base", 'tmp');    
    // or you could use:
    // $flatcon = new DocumentStoreOne(__DIR__ . "/base", 'tmp');  
} catch (Exception $e) {
    die("Unable to create document store. Please, check the folder");
}
$flatcon->insertOrUpdate("somekey1",json_encode(array("a1"=>'hello',"a2"=>'world'))); // or you could use serialize/igbinary_serialize
$doc=$flatcon->get("somekey1");
$listKeys=$flatcon->select();
$flatcon->delete("somekey1");
include "lib/DocumentStoreOne.php";
use eftec\DocumentStoreOne\DocumentStoreOne;
$doc=new DocumentStoreOne("base","task",'folder');
//also: $doc=new DocumentStoreOne(__DIR__."/base","task",'folder');
$doc->serializeStrategy='php'; // it sets the strategy of serialization to php
$doc->autoSerialize(true); // autoserialize

$flatcon->insertOrUpdate("somekey1",array("a1"=>'hello',"a2"=>'world')); 

方法

构造函数($baseFolder,$collection,$strategy=DocumentStoreOne::DSO_AUTO,$server="",$serializeStrategy = false,$keyEncryption = '')

创建DocumentStoreOne实例。

  • $baseFolder:应为一个文件夹
  • $collection:(一个子文件夹)是可选的。
  • $strategy:这是确定文件是否正在使用中的策略。
  • $server:由REDIS使用。您可以为策略设置使用的服务器。
  • $serializeStrategy:如果为false,则不序列化信息。

示例

$flatcon = new DocumentStoreOne(__DIR__ . "/base"); // new instance, using the folder /base, without serialization and with the default data

$flatcon = new DocumentStoreOne(__DIR__ . "/base", '','auto','','php_array'); // new instance and serializing using php_array

基准测试添加100个插入所需时间(以秒为单位)。

use eftec\DocumentStoreOne\DocumentStoreOne;
include "lib/DocumentStoreOne.php";
try {
    $flatcon = new DocumentStoreOne(__DIR__ . "/base", 'tmp');
} catch (Exception $e) {
    die("Unable to create document store.".$e->getMessage());
}
use eftec\DocumentStoreOne\DocumentStoreOne;
include "lib/DocumentStoreOne.php";
try {
    $flatcon = new DocumentStoreOne("/base", 'tmp',DocumentStoreOne::DSO_APCU);
} catch (Exception $e) {
    die("Unable to create document store.".$e->getMessage());
}

isCollection($collection)

如果集合有效(子文件夹),则返回true。

$ok=$flatcon->isCollection('tmp');

collection($collection)

设置当前集合。

$flatcon->collection('newcollection'); // it sets a collection.

此命令可以是嵌套的。

$flatcon->collection('newcollection')->select(); // it sets and return a query

注意,它不会验证集合是否正确或存在。您必须使用isCollection()来验证它是否正确。

autoSerialize($value=true,$strategy='php')

设置是否自动序列化信息,以及如何序列化。您也可以在构造函数中设置。

createCollection($collection)

创建一个集合(基础文件夹内的一个新文件夹)。如果操作失败,则返回false;否则返回true。

$flatcon->createCollection('newcollection'); 
$flatcon->createCollection('/folder1/folder2'); 

insertOrUpdate($id,$document,[$tries=-1])

在指示的$id中插入一个新的文档(字符串)。如果文档存在,则更新它。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

// if we are not using auto serialization
$doc=json_encode(["a1"=>'hello',"a2"=>'world']);
$flatcon->insertOrUpdate("1",$doc); // it will create a document called 1.dson in the base folder.

// if we are using auto serialization
$flatcon->insertOrUpdate("1",["a1"=>'hello',"a2"=>'world']);

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

比插入或更新更快。

insert($id,$document,[$tries=-1])

在指示的$id中插入一个新的文档(字符串)。如果文档存在,则返回false。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

// if we are not using auto serialization
$doc=json_encode(array("a1"=>'hello',"a2"=>'world'));
$flatcon->insert("1",$doc);

// if we are using auto serialization
$flatcon->insert("1",["a1"=>'hello',"a2"=>'world']);

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

update($id,$document,[$tries=-1])

更新指示的文档(字符串)。如果文档不存在,则返回false。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

// if we are not using auto serialization
$doc=json_encode(["a1"=>'hello',"a2"=>'world']);
$flatcon->update("1",$doc);
// if we are using auto serialization
$flatcon->update("1",["a1"=>'hello',"a2"=>'world']);

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

get($id,[$tries=-1],$default=false)

读取文档$id。如果文档不存在,或无法读取,则返回false。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

$doc=$flatcon->get("1"); // the default value is false

$doc=$flatcon->get("1",-1,'empty');

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

getFiltered($id,[$tries=-1],$default=false,$condition=[],$reindex=true)

读取过滤的文档$id。如果文档不存在,或无法读取,则返回false。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

// data in rows [['id'=>1,'cat'=>'vip'],['id'=>2,'cat'=>'vip'],['id'=>3,'cat'=>'normal']];
$data=$this->getFiltered('rows',-1,false,['cat'=>'normal']); // [['id'=>3,'cat'=>'normal']]
$data=$this->getFiltered('rows',-1,false,['type'=>'busy'],false); // [2=>['id'=>3,'cat'=>'normal']]

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

public function appendValue($name,$addValue,$tries=-1)

向具有名称$name的文档中添加一个值。新值被添加,因此它避免了创建整个文档。例如,对于日志文件很有用。

a) 如果值不存在,则使用$addValue创建它,否则返回true。
b) 如果值存在,则添加$addValue,并返回true。
c) 否则返回false。

$seq=$flatcon->appendValue("log",date('c')." new log");

getNextSequence($name="seq",$tries=-1,$init=1,$interval=1,$reserveAdditional=0)

读取或生成一个新的序列。

a) 如果序列存在,则它将增加$interval并返回此值。
b) 如果序列不存在,则使用$init创建,并返回此值。c) 如果库无法创建序列、无法锁定或序列存在但无法读取,则返回false。

$seq=$flatcon->getNextSequence();

您可以使用$id=get('genseq_')来查看序列,但并不推荐。

如果序列损坏,则将其重置为$init。

如果您需要保留一系列序列,则可以使用$reserveAdditional

$seq=$flatcon->getNextSequence("seq",-1,1,1,100); // if $seq=1, then it's reserved up to the 101. The next value will be 102.

getSequencePHP()

基于时间、一个随机值和serverId返回一个唯一的序列(64位整数)。

碰撞(生成相同值)的概率为1/4095(每0.0001秒执行两次操作)。

$this->nodeId=1; // if it is not set then it uses a random value each time.
$unique=$flatcon->getSequencePHP(); 

ifExist($id,[$tries=-1])

检查文档$id是否存在。如果文档存在,则返回true。否则返回false。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

只有当文档完全解锁时,才会进行验证。

$found=$flatcon->ifExist("1");

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

delete($id,[$tries=-1])

删除文档$id。如果文档不存在或无法删除,则返回false。
$tries表示尝试次数。默认值为-1(默认尝试次数)。

$doc=$flatcon->delete("1");

如果文档被锁定,则它将重试,直到可用或尝试“nth”次数(默认为100次,相当于10秒)。

select($mask="*")

返回存储在集合上的所有ID。

$listKeys=$flatcon->select();
$listKeys=$flatcon->select("invoice_*");

包括锁定文档。

copy($idorigin,$iddestination,[$tries=-1])

将文档$idorigin复制到$iddestination

$bool=$flatcon->copy(20,30);

如果目标文档存在,则将其替换。

rename($idorigin,$iddestination,[$tries=-1])

将文档$idorigin重命名为$iddestination

$bool=$flatcon->rename(20,30);

如果目标文档存在,则操作失败。

fixCast (实用类)

将stdclass转换为特定类。

$inv=new Invoice();
$invTmp=$doc->get('someid'); //$invTmp is a stdClass();
DocumentStoreOne::fixCast($inv,$invTmp); 

它不适用于数组对象成员。数组保持为stdclass。

DocumentStoreOne 字段

以下字段是公共的,它们可以在运行时更改。

示例

$ds=new DocumentStoreOne();
$ds->maxLockTime=300;
$ds=new DocumentStoreOne();
$ds->insert('1','hello'); // it stores the document 1.dson
$ds->keyEncryption='SHA256';
$ds->insert('1','hello'); // it stores the document 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b.dson

MapReduce

可以手动完成。系统允许存储预先计算好的值,可以轻松访问(而不是读取所有值)。

比如说下一个练习,我们有一个购买清单

约翰购买了3个编码为33的产品。产品33的单价为23.3美元。

问题,每位顾客支付了多少钱?

这是一个简单的练习,它更适合关系型数据库(select * from purchases inner join products)。然而,如果文档很长或复杂,难以存储在数据库中,那么文档存储在这里就很有优势。

// 1) open the store
$ds=new DocumentStoreOne('base','purchases'); // we open the document store and selected the collection purchase.
$ds->autoSerialize(true,'auto');
// 2) reading all products
// if the list of products holds in memory then, we could store the whole list in a single document (listproducts key)
$products=$ds->collection('products')->get('listproducts');
// 3) we read the keys of every purchases. It could be slow and it should be a limited set (<100k rows)    
$purchases=$ds->collection('purchases')->select(); // they are keys such as 14,15...

$customerXPurchase=[];
// 4) We read every purchase. It is also slow.  Then we merge the result and obtained the final result
foreach($purchases as $k) {
    $purchase=$ds->get($k);
    @$customerXPurchase[$purchase->customer]+=($purchase->amount * @$products[$purchase->productpurchase]); // we add the amount
}
// 5) Finally, we store the result.
$ds->collection('total')->insertOrUpdate($customerXPurchase,'customerXPurchase'); // we store the result.

由于这是在代码中完成的,因此可以创建一个混合系统(关系型数据库+存储+内存缓存)

限制

  • 键应该是类型A-a,0-9。在Windows中,键不区分大小写。
  • 一个集合可以容纳的文档数量取决于所使用的文档系统。NTFS允许每个集合有200万个文档。

序列化策略

假设我们想要序列化以下信息

$input=[['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']];

NONE

值没有被序列化,所以无法序列化对象、数组或其他结构。它只适用于字符串。

值是如何存储的

helloworld

值是如何返回的

"helloworld"

PHP

PHP的序列化是一种快速序列化和反序列化的方法,并且始终以相同的结构(类、数组、字段)返回相同的值

然而,存储的值可能很长。

值是如何存储的

a:2:{i:0;a:2:{s:2:"a1";i:1;s:2:"a2";s:1:"a";}i:1;a:2:{s:2:"a1";i:2;s:2:"a2";s:1:"b";}}

值是如何返回的

[['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']]

PHP_ARRAY

这种序列化生成PHP代码。这段代码很冗长,但也有一些不错的功能

  • 它可以被PHP的OPcache缓存。
  • 它加载速度快。

值是如何存储的

<?php /** @generated */
return array (
  0 => 
  array (
    'a1' => 1,
    'a2' => 'a',
  ),
  1 => 
  array (
    'a1' => 2,
    'a2' => 'b',
  ),
);

值是如何返回的

[['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']]

JSON_ARRAY 和 JSON_OBJECT

两种方法都使用JSON进行序列化和反序列化,但第一种总是返回关联数组,而另一种可以返回对象(stdClass)

优点

  • JSON速度快(但不如PHP的序列化快)
  • JSON跨平台兼容性好。
  • JSON使用的空间比PHP的序列化少。

缺点

  • 它比PHP的序列化慢得多
  • 结果可能不同,它可能返回不同的结构(对象总是返回stdClass)

值是如何存储的

[{"a1":1,"a2":"a"},{"a1":2,"a2":"b"}]

值是如何返回的

[['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']] // array
[stdClass{'a1'=>1,'a2'=>'a'},stdClass{'a1'=>2,'a2'=>'b'}] // object

错误控制

默认情况下,这个库在发生错误或异常时抛出错误。一些方法允许避免抛出错误,但大多数方法可能会抛出错误。

错误是try/catch可捕获的。

// throw an error:
$this->throwable=true; // (default value is true) If false, then the errors are stored in $this->latestError
try {
    $this->insert('id1','values'); 
} catch($ex) {
    var_dump($ex);
}
// not throw an error:
$this->throwable=false; 
$this->insert('id1','values'); 
if($this->latestError) {
	var_dump($this->latestError);
}
$this->resetError();

或者您也可以使用它来避免抛出异常

// not throw an error:
$this->->noThrowOnError()->insert('id1','values'); 
// but you can still see the latest error:
if($this->latestError) {
	var_dump($this->latestError);
}

与CSV一起工作

您可以使用以下方式与CSV一起工作

$doc=new DocumentStoreOne(__DIR__ . "/base",'','none','','csv'); // set your strategy to csv.
$doc->docExt='.csv'; // (optional), you can set the extension of the document
$doc->csvPrefixColumn='col_'; // (optional), you can set the name of the columns (if the csv doesn't have columns)
$doc->csvStyle(); // (optional) not needing, but you can use to set your own specifications of csv, for example tab-separated, etc.
$doc->regionalStyle(); // (optional) not needing, but you can use to set your own regional settings.
$values=[
    ['name'=>'john1','age'=>22],
    ['name'=>'john2','age'=>22],
    ['name'=>'john3','age'=>22],
    ];
$doc->delete('csv1');
$doc->insert('csv1',$values);

版本列表

  • 1.27 2027-07-19
    • 修复了解锁问题(文件夹策略),当文件夹不再存在时。
  • 1.26 2024-02-13
    • composer.json建议ig-binary,不是必需的
  • 1.25.1 2023-06-04
    • 修复了构造函数中的bug。现在,如果它们不存在,它会生成文件夹。
  • 1.25 2023-06-04
    • 添加了DocumentStoreOne::isRelativePath()
    • 现在您可以在构造函数中指定相对路径
  • 1.24 2022-06-29
    • deleteCollection()删除集合及其内容。
  • 1.23 2022-03-20
    • [新功能] 允许通过静态方法DocumentStoreOne::instance()获取DocumentStoreOne的实例(如果有)
  • 1.22.1 2022-03-12
    • getTimeStamp()修复:如果文件不存在,则返回警告。
  • 1.22 2022-03-12
    • 添加了setTimeStamp()
  • 1.21 2022-02-07
    • 兼容PHP 7.2及以上版本。此库不再与PHP 5.6兼容,但可以使用库的旧版本。
    • 测试了与PHP 8.1的兼容性
    • [新功能] 方法noThrowOnError()
  • 1.20 2021-12-11
    • 添加igbinary
  • 1.19 2021-12-08
    • [新功能] 更多的错误控制。
  • 1.18 2021-12-08
    • [新功能] 添加csv作为序列化策略
    • 一些优化
    • 移除了Memcache。
  • 1.16.2 2020-09-20
    • getTimeStamp()在文件不存在时抛出异常。现在它返回false。
  • 1.16 2020-09-20
    • 新方法getTimeStamp()
  • 1.15 2020-09-13
    • 方法get()现在正确解锁文档(使用php_array方法)
    • 方法appendValue()对json_object、json_array更有效,并支持php_array。
    • 方法 appendValue() 现在生成一个值数组。
  • 1.14 2020-09-13
    • 修复了 composer.json。但是,之前的 composer.json 导致了安装问题,因此已从 packagist 中删除所有之前的版本。
    • 你可能需要删除 "composer.lock" 和文件夹 vendor\efted\documentstoreone,然后运行 composer update。

[RuntimeException] 无法在 repo.packagist.org 加载包 eftec/documentstoreone:[UnexpectedValueException] 无法解析版本约束 ^5.6.:无效的版本字符串 "^5.6."

  • 1.13 2020-07-12
    • 方法 appendValue() 现在序列化信息,并可与大多数方法(但不是 php_array)一起工作。
  • 1.12 2020-04-18
    • 方法 get() 有一个默认值
    • 方法 unlock() 移除了参数 $forced
    • 新增方法 getFiltered()
  • 1.11 2019-10-23
    • 新增方法 setObjectIndex() 它设置了 insertObject() 和 insertOrUpdateObject() 的默认索引字段
    • 新增方法 insertObject()
    • 新增方法 insertOrUpdateObject()
    • 方法 select() 现在可以返回一组文档的索引列表
  • 1.10 2019-08-30 一些清理。添加了 getSequencePHP() 和 nodeId 字段
  • 1.9 2019-02-10 解锁现在尝试解锁。不再使用 manuallock 字段。
  • 1.8 2018-02-03 添加了 neverLock 字段(用于快速访问只读数据库)以及 phpunit
  • 1.7.3 2018-02-03 更新了 composer.json
  • 1.7.1 2018-10-20 删除了 lock() 中的不正确 echo
  • 1.7 2018-10-20 添加了密钥加密(可选)
  • 1.6 2018-10-19
    • 由于通常 PHP 配置为 30 秒的超时时间,因此将默认时间从 30 秒减少到 10 秒。
    • 方法 ifExist 锁定资源并永不释放。现在按预期释放。
  • 1.5 2018-10-13 维护更新。修复了自动策略
  • 1.4 2018-08-26 函数 rename
  • 1.3 2018-08-15 添加了锁定策略。
  • 1.2 2018-08-12 小型修复。
  • 1.1 2018-08-12 将架构更改为 collection。
  • 1.0 2018-08-11 第一个版本

待定

  • 事务性(允许提交或回滚多步事务)。目前处于评估阶段。
  • 不同的锁定策略(文件夹、redis 和 apcu)
  • Msgpack 和 igbinary