EOS内存数据库chainbase深入解析
简介
eos内存数据库的用来保存区块链执行之后的所有状态,如果把区块链看成是操作日志,那么eos内存数据库就是用来记录操作之后的系统状态。
-
eos内存数据库保存着eos系统的所有状态
一个transaction中的action可能会在最后执行失败但action已经对数据库做了操作,区块链本身也可能存在分叉,这就要求数据库有一种任意回滚的能力,eos内存数据库提供了这种能力,通过start_undo_session开启一个undo session(回滚session),在这个undo session内的所有操作都可以通过undo撤销,也可以通过commit弹出这个undo session使状态变成不可撤销。
eos中的database
在分析eos database时候手上的eos代码版本为Version 1.1.3,eos database的实现源码在 eos/libraries/chainbase
目录下的chainbase.hpp,chainbase.cpp。database的主要是实现是通过boost::multi_index_container
和 boost::interprocess::managed_mapped_file
,multi_index_container 用来做容器实现增删改查,managed_mapped_file实现内存映射文件并为multi_index_container 提供内存分配器allocator。
-
multi_index_container
boost实现的一个多个关键字索引容器,通俗的理解你把它看成一张数据库中的表,eos中核心的表一共有20多张(controller.cpp中add_indices函数),其中eos合约开发数据库操作相关的有2张表table_id_multi_index
和key_value_index
,eos合约下面的eosiolib/multi_index.hpp仅仅是给eos合约开发提供的一个数据库操作接口,最终的所有操作都会映射到table_id_multi_index,key_value_index这2个multi_index_container中去, 这2张表,table_id_multi_index保存着所有eos合约中创建的表信息,key_value_index保存着所有eos合约中创建的表记录(在eos合约中不同表创建的记录)最终都会保存到该表中。 -
undo session
undo session这个概念是eos建立在multi_index_container之上用于执行回滚操作,实现的大体思路为:当创建一个undo session之后对增,删,改操作进行跟踪,用户如果增加了记录row,就记录该记录row的id,在undo的时候删除该记录row,用户如果删除了记录row,就记录该记录row,在undo的时候还原该row,用户如果修改了记录row,就保存修改之前的记录row,在undo的时候还原该row。当用户执行undo的时候用保存的undo状态信息还原,当用户进行commit的时候抛弃保存的undo状态信息使修改变得不可逆 -
database中的增删改查和undo session主要用过generic_index类实现,下面 对一些主要接口做说明,具体代码自己去看源码。所有的undo session的undo状态信息保存在下面的generic_index的_stack中,这些状态信息串在一起,可以进行回退撤销,有点像git提交历史可以回退。
template<typename MultiIndexType>
class generic_index
{
public:
typedef bip::managed_mapped_file::segment_manager segment_manager_type;
typedef MultiIndexType index_type;
typedef typename index_type::value_type value_type;
typedef bip::allocator< generic_index, segment_manager_type > allocator_type;
typedef undo_state< value_type > undo_state_type;
// 增
template<typename Constructor>
const value_type& emplace( Constructor&& c );
// 改
template<typename Modifier>
void modify( const value_type& obj, Modifier&& m );
// 删
void remove( const value_type& obj );
template<typename CompatibleKey>
// 查
const value_type* find( CompatibleKey&& key );
// 创建一个undo session为后面进行undo撤销或者commit永久保存
session start_undo_session( bool enabled );
// 撤销最新一个undo_session
void undo();
// 合并最新的2个undo_session,这个功能主要用于push_transactions
void squash();
// 放弃revision之前所有的undo_session,这样这些更改变得不可撤销
void commit( int64_t revision );
private:
// 监听修改操作 放入undo_state对象,当需要undo撤销的时候还原被修改的对象
void on_modify( const value_type& v );
// 监听删除操作 放入undo_state对象,当需要undo撤销的时候添加被删除的对象
void on_remove( const value_type& v );
// 监听创建操作,放入undo_state对象,当需要undo撤销的时候删除呗创建的对象
void on_create( const value_type& v );
// _stack串联起undo_state对象,undo撤销session的就靠这些对象还原
boost::interprocess::deque< undo_state_type, allocator<undo_state_type> > _stack;
// 版本号,等于block_num, 一个block_num对应一个undo_session, 最终由commit提交变成不可更改
int64_t _revision = 0;
// _indices下一个主键id
typename value_type::id_type _next_id = 0;
// _indices boost::multi_index_container对象
index_type _indices;
uint32_t _size_of_value_type = 0;
uint32_t _size_of_this = 0;
};
- 每次start_undo_session都会生成一个undo_state对象,该对象是实现undo撤销的关键,它里面保存了该undo session所有的新建,删除,修改对象
template< typename value_type >
class undo_state
{
public:
typedef typename value_type::id_type id_type;
typedef allocator< std::pair<const id_type, value_type> > id_value_allocator_type;
typedef allocator< id_type > id_allocator_type;
template<typename T>
undo_state( allocator<T> al )
:old_values( id_value_allocator_type( al.get_segment_manager() ) ),
removed_values( id_value_allocator_type( al.get_segment_manager() ) ),
new_ids( id_allocator_type( al.get_segment_manager() ) ){}
typedef boost::interprocess::map< id_type, value_type, std::less<id_type>, id_value_allocator_type > id_value_type_map;
typedef boost::interprocess::set< id_type, std::less<id_type>, id_allocator_type > id_type_set;
id_value_type_map old_values; // 保存了修改的对象字典,在undo撤销的时候还原这些对象 on_modify()
id_value_type_map removed_values; // 保存了删除的对象字典,在undo撤销的时候添加这些对象 on_remove()
id_type_set new_ids; // 保存新建的对象id集合,在undo撤销的时候删除这些对象 on_create()
id_type old_next_id = 0;
int64_t revision = 0; // undo_state序号,等于不可修改的block num
};
测试代码
- 下面是测试代码,编译指令
clang-4.0 -std=c++14 -I{yours}/boost/include -L{yours}/boost/lib -o test -lstdc++ -lpthread -lm -lboost_chrono -lboost_graph -lboost_system -lboost_chrono -lboost_filesystem -lboost_thread -lboost_exception test.cpp chainbase.cpp
。chainbase还是比较独立不依赖与eos中的其他部分,可以直接拿出来测试
#include "chainbase.hpp"
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/member.hpp>
#include <iostream>
#include <string>
using namespace chainbase;
using namespace boost::multi_index;
struct book : public chainbase::object<0, book> {
book() = delete;
public:
template<typename Constructor, typename Allocator>
book(Constructor&& c, chainbase::allocator<Allocator> a):name(a),author(a) { c(*this); }
id_type id;
shared_string name;
shared_string author;
int pulish_time;
};
std::ostream& operator<<(std::ostream& os,const book& bk)
{
os<<bk.id<<" "<<bk.name<<" "<<bk.author<<" "<<bk.pulish_time<<" "<<std::endl;
return os;
}
struct by_id;
struct by_pulish_time;
using book_index = chainbase::shared_multi_index_container<
book,
indexed_by<
ordered_unique<tag<by_id>, member<book, book::id_type, &book::id>>,
ordered_non_unique<tag<by_pulish_time>, BOOST_MULTI_INDEX_MEMBER(book,int,pulish_time) >
>
>;
//CHAINBASE_SET_INDEX_TYPE(book, book_index)
namespace chainbase {
template<> struct get_index_type<book>
{ typedef book_index type; };
}
void func(chainbase::database& db)
{
const auto& nb = db.create<book>([](book& b) {
b.name = "《1984》";
b.author = "乔治·奥威尔";
b.pulish_time = 1949;
});
const auto& nb2 = db.create<book>([](book& b) {
b.name = "《动物庄园》";
b.author = "乔治·奥威尔";
b.pulish_time = 1945;
});
auto session = db.start_undo_session(true);
db.modify( nb2, [&]( book& b ) {
b.name = "《xxxxx》";
});
std::cout << nb2;
db.undo();
std::cout << nb2 << std::endl;
}
int main()
{
chainbase::database db("data", database::read_write, 1024*1024*100, true);
db.add_index<book_index>();
func(db);
//std::cout << *db.find<book, by_id>(0);
//std::cout << *db.find(book::id_type(0));
const auto& book_id_index = db.get_index<book_index, by_id>();
//std::cout << *book_id_index.find(0);
copy(book_id_index.begin(),book_id_index.end(), std::ostream_iterator<book>(std::cout));
std::cout << std::endl;
const auto& book_pulish_time_index = db.get_index<book_index, by_pulish_time>();
copy(book_pulish_time_index.begin(),book_pulish_time_index.end(), std::ostream_iterator<book>(std::cout));
std::cout << std::endl;
// 索引的记录数
database::database_index_row_count_multiset dircm = db.row_count_per_index();
int num;
for(auto i:dircm)
{
std::cout<<i.second<<": "<<i.first<<std::endl;
num = i.first;
}
std::cout<<"free_memory: "<<db.get_free_memory()<<std::endl;
std::cout<<"is_read_only: "<<db.is_read_only()<<std::endl;
std::cout<<"revision: "<<db.revision()<<std::endl;
//db.remove(*book_id_index.find( 0 ));
db.flush();
return 0;
}
输出结果:
chainbase::oid<book>(1) 《xxxxx》 乔治·奥威尔 1945
chainbase::oid<book>(1) 《动物庄园》 乔治·奥威尔 1945
chainbase::oid<book>(0) 《1984》 乔治·奥威尔 1949
chainbase::oid<book>(1) 《动物庄园》 乔治·奥威尔 1945
chainbase::oid<book>(1) 《动物庄园》 乔治·奥威尔 1945
chainbase::oid<book>(0) 《1984》 乔治·奥威尔 1949
book: 2
free_memory: 104855792
is_read_only: 0
revision: 0
eos内存数据库在区块链中的使用
- 在controller.cpp中的start_block函数中创建一个针对该区块的block undo session
void start_block( block_timestamp_type when, uint16_t confirm_block_count, controller::block_status s ) {
EOS_ASSERT( !pending, block_validate_exception, "pending block is not available" );
...
//创建block undo session
pending = db.start_undo_session(true);
...
}
- 在transaction_context.cpp中的transaction_context构造函数中创建一个transaction undo session
transaction_context::transaction_context( controller& c,
const signed_transaction& t,
const transaction_id_type& trx_id,
fc::time_point s )
...
//创建transaction undo session
,undo_session(c.db().start_undo_session(true))
...
-
在controller.cpp中 push_transaction函数中执行完一个transaction后对上述transaction undo session进行undo()或者squash(),undo很好理解就是撤销,squash这里是把该undo session合并当之前一个undo session,之前一个undo session就是block undo session,最终由block undo session来决定是否需要commit或者undo
-
当一个块不可逆的情况下执行block undo session的commit
void on_irreversible( const block_state_ptr& s ) {
...
db.commit( s->block_num );
...
}
最后
chainbase通过managed_mapped_file内存映射文件,把内存数据映射到一个文件中去(实现持久化),可用通过flush()函数显示的把内存同步中去,chainbase可用容量大小完全取决于系统中物理内存的多少,好在chainbase存储的是系统状态的核心数据,不会存放例如用户的媒体数据所以目前阶段不必担心数据库容量不够的问题。chainbase通过managed_mapped_file内存映射文件映射的文件本身可能不可以拷贝到其他系统中去用,chainbase也可能在突然宕机之后内存文件可能出现损坏,不过这些都不是最重要的问题因为都可以通过replay区块链来重新获得