eftec / documentstoreone
一个支持多并发的PHP扁平文档存储
Requires
- php: >=7.2
- ext-json: *
Requires (Dev)
- ext-apcu: *
- ext-redis: *
- phpunit/phpunit: >=9.5.13
Suggests
- ext-apcu: If you want to use apcu for locks. It is recommended
- ext-igbinary: Some examples could use it
- ext-redis: If you want to use redis for locks. It is not recommended becuase it's slow than folder
README
一个支持多并发的PHP文档存储。它是一个MongoDB或CouchDB的轻量级替代品,无需安装新服务。
它还可以作为一个小型的数据库使用。
- DocumentStoreOne
- 关键特性
- 测试
- 并发测试
- 使用方法
- 方法
- 构造函数($baseFolder,$collection,$strategy=DocumentStoreOne::DSO_AUTO,$server="",$serializeStrategy = false,$keyEncryption = '')
- isCollection($collection)
- collection($collection)
- autoSerialize($value=true,$strategy='php')
- createCollection($collection)
- insertOrUpdate($id,$document,[$tries=-1])
- insert($id,$document,[$tries=-1])
- update($id,$document,[$tries=-1])
- get($id,[$tries=-1],$default=false)
- getFiltered($id,[$tries=-1],$default=false,$condition=[],$reindex=true)
- public function appendValue($name,$addValue,$tries=-1)
- getNextSequence($name="seq",$tries=-1,$init=1,$interval=1,$reserveAdditional=0)
- getSequencePHP()
- ifExist($id,[$tries=-1])
- delete($id,[$tries=-1])
- select($mask="*")
- copy($idorigin,$iddestination,[$tries=-1])
- rename($idorigin,$iddestination,[$tries=-1])
- fixCast (实用类)
- DocumentStoreOne 字段
- MapReduce
- 限制
- 序列化策略
- 错误控制
- 与CSV一起工作
- 版本列表
关键特性
- 基于单个键。
- 速度快。然而,它不是关系型数据库的替代品。它优化了存储一定数量的文档而不是数百万行。
- 通过锁定和解锁文档来支持多并发。如果文档被锁定,则重试直到文档解锁或重试次数达到限制。
- 一个没有依赖的单个类。
- 自动解锁被锁定的文档(默认情况下,如果文件被锁定,则每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秒。
并发测试
进行了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