Chainbase简介
chainbase是eosio开发的高性能的内存映射数据库。其核心是使用内存文件映射技术来实现的,最终实现了可以将文件的内容自动映射到内存中,可以说实现了修改,访问,保存的自动映射,使用的相关头文件如下
#include <boost/interprocess/allocators/node_allocator.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/segment_manager.hpp>
#include <boost/interprocess/containers/vector.hpp>
整个chainbase的实现与定义全部浓缩在chainbase.hpp, chain base.cpp文件中,使用灵活的模版类实现,具有很高的扩展性。
chainbase的核心类
database: 整个chainbase数据库访问的入口,核心接口如下
- start_undo_session:开启一个回滚回话,相当于建立一个回滚点
- revision: 返回当前的回滚版本号,也就是块号
- undo:撤销自上个版本上所有的修改
- squash: 预提交,目的是合并最近的两次提交记录,有点像git的squash,会把提交的记录树,合并成最高只有两层的记录树
- commit:正式确认提交,提交之后的数据是不能修改与回滚的
- set_revision: 设置当前操作的版本号
- add_index: 模版函数,创建模版参数对应的内存表
- find: 根据key查找对象,找不到返回null
- get:根据key获取对象,找不到抛出异常
- modify: 修改一个对象
- remove:删除一个对象
- create:创建一个对象
abstract_index:单张表的操作接口,我们创建的单个对象的表就是在这个类里面
abstract_index接口对应数据库的增删改查,代码如下
class abstract_index
{
public:
abstract_index( void* i ):_idx_ptr(i){
}
virtual ~abstract_index(){
}
virtual void set_revision( uint64_t revision ) = 0;
virtual unique_ptr<abstract_session> start_undo_session( bool enabled ) = 0;
virtual int64_t revision()const = 0;
virtual void undo()const = 0;
virtual void squash()const = 0;
virtual void commit( int64_t revision )const = 0;
virtual void undo_all()const = 0;
virtual void remove_object( int64_t id ) = 0;
};
undo_state:回滚状态,记录了回滚操作需要的相关信心
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; //对象发生修改时,记录的旧值,回滚时用以恢复旧值
id_value_type_map removed_values; //对象被删除时,记录改值,回滚时重新添加该值
id_type_set new_ids; //添加的新对象,回滚时需要删除改新对象
id_type old_next_id = 0; //记录创建回滚节点是的上一次块号
int64_t revision = 0; //当前回滚块号,在按照块号回滚石,就是使用这个数值进行判断的
};
generic_index:表操作的实现类
这个类中,最核心的是在实现数据操作的同时,也记录了修改数据的操作,利用这些操作记录实现了回滚
EOSIO中Chainbase的应用
eosio既然使用了chainbase,那么在整个eosio中,系统使用chainbase都记录了哪些数据,相关表的代码定义如下
// 记录了账户,资源,合约,已经生产相关的数据
// 注意:table_id_multi_index记录创建了那些合约表
using controller_index_set = index_set<
account_index,
account_metadata_index,
account_ram_correction_index,
global_property_multi_index,
protocol_state_multi_index,
dynamic_global_property_multi_index,
block_summary_multi_index,
transaction_multi_index,
generated_transaction_multi_index,
table_id_multi_index,
code_index,
database_header_multi_index
>;
// 合约状态数据表相关
// key_value_index是核心
using contract_database_index_set = index_set<
key_value_index,
index64_index,
index128_index,
index256_index,
index_double_index,
index_long_double_index
>;
// 账户权限相关表
using authorization_index_set = index_set<
permission_index,
permission_usage_index,
permission_link_index
>;
上面是eosio中系统创建的所有表,那它是什么时候被创建的,我们只列举其中一个为例,代码如下
// 以账户权限表为例
void authorization_manager::add_indices() {
authorization_index_set::add_indices(_db);
}
// 继续进入代码如下
// 注意: 最终调用代码db.add_index函数,回顾上面的类容
template<typename Index>
class index_set<Index> {
public:
static void add_indices( chainbase::database& db ) {
db.add_index<Index>();
}
};
template<typename FirstIndex, typename ...RemainingIndices>
class index_set<FirstIndex, RemainingIndices...> {
public:
static void add_indices( chainbase::database& db ) {
index_set<FirstIndex>::add_indices(db);
index_set<RemainingIndices...>::add_indices(db);
}
};
通过跟踪我们会发现,在系统启动初始化时,会分别创建对应的表,最终将表建立在chainbase文件中。
EOSIO中的数据存储
从上面的章节我们可以分析出,eosio对数据的存储,总结起来可以分为两类,一类是预先定义好的对象,如账户,权限等数据,还有一类是合约状态数据,这类数据系统是不知道,那么在eosio中,是如何处理这两类数据的呢
普通对象的存储
我们还是以账户数据为例,账户数据表定义如下
class account_object : public chainbase::object<account_object_type, account_object> {
OBJECT_CTOR(account_object,(abi))
id_type id;
account_name name; //< name should not be changed within a chainbase modifier lambda
block_timestamp_type creation_date;
shared_blob abi;
};
using account_id_type = account_object::id_type;
struct by_name;
using account_index = chainbase::shared_multi_index_container<
account_object,
indexed_by<
ordered_unique<tag<by_id>, member<account_object, account_object::id_type, &account_object::id>>,
ordered_unique<tag<by_name>, member<account_object, account_name, &account_object::name>>
>
>;
// 最终表创建时,调用如以下格式
db.add_index<account_index>();
从上述代码中,我们可以看到一些结果:
- 定义对象account_object,并且对象继承chainbase::object,从account_object对象看,我们能清晰的看出对象对应的字段信息
- 定义对象容器account_index,来存储account_object对象,同时还定义了对象存储的排序规则,用于后面对象的检索
- 在对象容器中,最终实际上是用了boost的boost::multi_index_container多索引容器,从这里我们也可以看到,在eosio中对象表,实际上就是一个多索引容器,在对象容器上定义不同的索引对象,来便于我们存储,查询等操作
在这里我们回头看chainbase::add_index函数,核心功能如下,代码注释如下
template<typename MultiIndexType>
void add_index() {
const uint16_t type_id = generic_index<MultiIndexType>::value_type::type_id;
typedef generic_index<MultiIndexType> index_type;
typedef typename index_type::allocator_type index_alloc;
std::string type_name = boost::core::demangle( typeid( typename index_type::value_type ).name() );
// 判断类型是否合法,表是否存在
if( !( _index_map.size() <= type_id || _index_map[ type_id ] == nullptr ) ) {
BOOST_THROW_EXCEPTION( std::logic_error( type_name + "::type_id is already in use" ) );
}
// 创建该表的内存segment,建立内存与文件的映射关系
index_type* idx_ptr = nullptr;
if( _read_only )
idx_ptr = _db_file.get_segment_manager()->find_no_lock< index_type >( type_name.c_str() ).first;
else
idx_ptr = _db_file.get_segment_manager()->find< index_type >( type_name.c_str() ).first;
bool first_time_adding = false;
if( !idx_ptr ) {
if( _read_only ) {
BOOST_THROW_EXCEPTION( std::runtime_error( "unable to find index for " + type_name + " in read only database" ) );
}
first_time_adding = true;
idx_ptr = _db_file.get_segment_manager()->construct< index_type >( type_name.c_str() )( index_alloc( _db_file.get_segment_manager() ) );
}
// 将创建出来的新表,加入_index_list对象
auto new_index = new index<index_type>( *idx_ptr );
_index_map[ type_id ].reset( new_index );
_index_list.push_back( new_index );
}
那么对于这个对象表,程序是如何访问的呢? 账户权限代码如下
// 添加account_object对象
const auto& new_account = db.create<account_object>([&](auto& a) {
a.name = create.name;
a.creation_date = context.control.pending_block_time();
});
// 添加permission_object
const auto& perm = _db.create<permission_object>([&](auto& p) {
p.usage_id = perm_usage.id;
p.parent = parent;
p.owner = account;
p.name = name;
p.last_updated = creation_time;
p.auth = std::move(auth);
});
// 修改permission_object
_db.modify( permission, [&](permission_object& po) {
po.auth = auth;
po.last_updated = _control.pending_block_time();
});
// 删除permission_object
_db.remove( permission );
合约数据的存储
用户的合约数据是用户定义定数据,是个不确定的数据定义,那么eosio是怎么解决的,先来看结构定义
/**
* 表对象定义,记录用户在合约中创建了多少个表
*/
class table_id_object : public chainbase::object<table_id_object_type, table_id_object> {
OBJECT_CTOR(table_id_object)
id_type id; //表ID
account_name code; //合约账户
scope_name scope; //表类型
table_name table; //表名
account_name payer; //创建者
uint32_t count = 0; /// the number of elements in the table
};
struct by_code_scope_table;
// 表对象表
using table_id_multi_index = chainbase::shared_multi_index_container<
table_id_object,
indexed_by<
ordered_unique<tag<by_id>,
member<table_id_object, table_id_object::id_type, &table_id_object::id>
>,
ordered_unique<tag<by_code_scope_table>,
composite_key< table_id_object,
member<table_id_object, account_name, &table_id_object::code>,
member<table_id_object, scope_name, &table_id_object::scope>,
member<table_id_object, table_name, &table_id_object::table>
>
>
>
>;
using table_id = table_id_object::id_type;
struct by_scope_primary;
struct by_scope_secondary;
struct by_scope_tertiary;
/**
* 合约记录数据状态对象
*/
struct key_value_object : public chainbase::object<key_value_object_type, key_value_object> {
OBJECT_CTOR(key_value_object, (value))
typedef uint64_t key_type;
static const int number_of_keys = 1;
id_type id; //记录ID
table_id t_id; //表ID,表明对象存在于那张表里面,对应table_id_object表
uint64_t primary_key; //记录主键
account_name payer; //谁操作的
shared_blob value; //数据内容,注意这里数据类型为shared_blob, 通过代码我们知道shared_blob为一段连续的二进制内存数据,所以我们确定合约的状态数据系统会将其序列化为shared_blob来存储,查询时反序列化给我们看
};
// 合约记录数据对象表
using key_value_index = chainbase::shared_multi_index_container<
key_value_object,
indexed_by<
ordered_unique<tag<by_id>, member<key_value_object, key_value_object::id_type, &key_value_object::id>>,
ordered_unique<tag<by_scope_primary>,
composite_key< key_value_object,
member<key_value_object, table_id, &key_value_object::t_id>,
member<key_value_object, uint64_t, &key_value_object::primary_key>
>,
composite_key_compare< std::less<table_id>, std::less<uint64_t> >
>
>
>;
看完合约状态数据表的定义,我们可以得出如下结论
- 系统单独记录了表对象数据
- 合约状态数据实际上是以系统定义的key-value结构来存储的
- 合约状态数据是序列化成 shared_blob二进制结构来存储的
对于合约状态数据,系统还定义了如index64_object类的定义,这类数据定义了状态表的第二索引,便于我们后期的查找,这里不做叙述
Chainbase回滚机制
要了解chainbase的回滚机制,必须要看它在创建,修改,删除时, 都做了哪些操作,同时在想想我们第一章节介绍的结构undo_state, 它是如何处理回滚的,接下来我们以插入新数据为例, 来分析chainbase的回滚机制
插入对象代码如下
template<typename ObjectType, typename Constructor>
const ObjectType& create( Constructor&& con )
{
CHAINBASE_REQUIRE_WRITE_LOCK("create", ObjectType);
typedef typename get_index_type<ObjectType>::type index_type;
// 最后调用函数 emplace
return get_mutable_index<index_type>().emplace( std::forward<Constructor>(con) );
}
template<typename Constructor>
const value_type& emplace( Constructor&& c ) {
auto new_id = _next_id;
// 构造新对象
auto constructor = [&]( value_type& v ) {
v.id = new_id;
c( v );
};
// 将对象放入容器
auto insert_result = _indices.emplace( constructor, _indices.get_allocator() );
// 如果失败,则抛出异常
if( !insert_result.second ) {
BOOST_THROW_EXCEPTION( std::logic_error("could not insert object, most likely a uniqueness constraint was violated") );
}
// 记录ID+1
++_next_id;
// 数据插入已经完成,接下来继续看on_create函数干了啥
on_create( *insert_result.first );
return *insert_result.first;
}
void on_create( const value_type& v ) {
// 如果不能修改,直接返回
if( !enabled() ) return;
// _stack: boost::interprocess::deque< undo_state_type, allocator<undo_state_type> > _stack;
// 可以看到_stack记录了一个操作命令表,类型为 undo_state_type
auto& head = _stack.back();
// 最后我们看到,chainbase将新添加对象的ID记录下来了
head.new_ids.insert( v.id );
}
至此,整个create过程完成,我们看到他在保存对象的同时,还记录了一个操作命令序列对象
那么它的undo是怎么做的呢,接下来继续看代码
// 下面的代码清晰的展示出,在进行回滚时,chainbase都做了哪些事情
void undo() {
if( !enabled() ) return;
// 获取当前操作记录的头节点
const auto& head = _stack.back();
// 对于新添加的对象要删除
for( auto id : head.new_ids )
{
_indices.erase( _indices.find( id ) );
}
// ID恢复
_next_id = head.old_next_id;
// 对于修改的对象,进行恢复
for( auto& item : head.old_values ) {
auto ok = _indices.modify( _indices.find( item.second.id ), [&]( value_type& v ) {
v = std::move( item.second );
});
if( !ok ) std::abort(); // uniqueness violation
}
// 对于已经删除掉的对象,重新添加回来
for( auto& item : head.removed_values ) {
bool ok = _indices.emplace( std::move( item.second ) ).second;
if( !ok ) std::abort(); // uniqueness violation
}
_stack.pop_back();
--_revision;
}
看懂了, 是不是很简单,每一步都要记录它的操作,回滚时执行与之对应的相反的操作
EOSIO整个系统的上层的回滚分为两大类
- 单交易回滚:在交易执行失败时,有析构函数完成undo,这个我们在前面提到过
- 块回滚:和块的生产,同步相关,后面我们再说
总结
本篇,我们详细分析了一下chainbase的数据库,它的结构,操作,以及他的回滚机制(类似git),我们在总结问题的同时也会产生如下疑问
- chainbase在初始化时,已经设置了文件大小,如果设置的太小,会发生什么事情? 有没有办法解决
- chainbase在存储数据时,是以二进制存储,那它的查询效率如何?
- 合约状态数据是以序列化存储进入chainbase的, 那么我们查询时他是如何反序列化的?同时它又时如何返回给合约的?相应的反序列化是在哪里做的?
- 如果我们要将chainbase升级成为关系数据库,需要注意哪些,如何实现它的回滚系统
等等,随着我们熟悉代码越深入,疑问也就越多,继续加油