EOSIO源码分析 - Chainbase运行原理分析

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升级成为关系数据库,需要注意哪些,如何实现它的回滚系统

等等,随着我们熟悉代码越深入,疑问也就越多,继续加油

猜你喜欢

转载自blog.csdn.net/whg1016/article/details/128716308