
首先下载Open VPN,然后使用附件的certs配置来配置open vpn,然后连接。下面是配置文件夹内的内容,基本把附件certs中的文件复制过去就好了。

然后打开finalshell,连接到对应的ip。
172.16.0.12 BtfQDDzWoYvCmiCXhm6f 172.16.0.67 23neMCCn5WhLbc4s6DWt
密码修改为:zhou19981209
参考 vscode连接远程服务器+SFTP同步本地文件 其实可以不用安装sftp,因为在vs code中可以直接上传本地文件和更改服务器文件
为了不用每次都输入密码,我们可以把公钥传输到远程服务器:

在authorized_keys中加入很多行公钥。然后把.ssh和authorized_keys的访问权限更新为至少可读。
查看系统架构:
1 | uname -a |
在执行sudo yum install rpm*时,出现了错误:

我直接跳过有依赖问题的包了,sudo yum install rpm* --skip-broken。


采用 sudo -E。简单来说,就是加上-E选项后,用户可以在sudo执行时保留当前用户已存在的环境变量,不会被sudo重置,另外,如果用户对于指定的环境变量没有权限,则会报错。
安装sysbench 1.0.20是可以的。直接在sysbench 上参考二进制的安装方法,推荐的那个。
在同一台电脑上添加多个ssh key - 简书 (jianshu.com)
我们调试的时候要在oceanbase的根目录下调试,然后设置断点的时候要指定函数所在的文件,例如:
1 | break ./src/sql/parser/ob_parser.cpp:ObParser::parse |
我们要把build_debug/src/observer/observer作为~/zhouhuahui/ob-advanced-data/bin/observer的软链接,才能使用gdb调试成功,或者直接在gdb下使用:
1 | dir /home/test/zhouhuahui/Github/oceanbase |
定位到源文件也是可以的。
如何在vs code上调试:参考 如何debug OceanBase。但是这种不够,我是是使用attach的方式来调试,写了下面的launch.json
1 | { |
在点击调试之后,在DEBUG CONSOLE界面输入:
1 | -exec dir /home/test/zhouhuahui/Github/oceanbase |
就可以正确定位到源文件的位置了。
如果还要使用launch的方式,要在程序的参数加一个”-N”参数,来指明程序在前台运行,(因为默认程序在后台运行),如果不加的话,会出先调试程序刚打开却退出的现象。
1 | { |
1 | obclient -uroot@test -h127.0.0.1 -P 2881 -c |
1 | use test; |
揭秘 OceanBase SQL 执行计划(一) (qq.com)
OceanBase SQL 执行计划解读(二)──── 表连接和子查询_OceanBaseGFBK的博客-CSDN博客
两个表join时,谁作为外部表,谁作为内部表不好说,由过滤条件应用后的结果集大小来定。
OceanBase 存储引擎高级技术 - 知乎 (zhihu.com)
OceanBase的索引创建流程 - 知乎 (zhihu.com)
MySQL中的semi-join_lppl010_的专栏-CSDN博客 讲解了什么是semi-join。
OB有两种从表中获取行的方法,一个是TABLE_SCAN,一个是TABLE_GET,为了方便,就直接说scan和get了。scan表示直接从头到尾扫描OB的索引组织表,然后获取这些行;get表示通过索引的key来找到这些行或者行的key。使用scan还是get是由SQL中的where部分决定的,假如查询条件涉及的列是有索引的,就可以使用get,假如没有,就只能使用scan。
但是还有一个问题是:假如A,B表进行嵌套循环连接,那么谁作外表,谁作内表呢?根据A,B获取行的方法不同,分为三种情况来讨论:
性能优化之Block Nested-Loop Join(BNL) - 云+社区 - 腾讯云 (tencent.com)
reset和reuse分别在哪些地方被用到了,reset和reuse的具体语义是什么,为什么要这么用?
reuse:
更改前
1 | namespace oceanbase { |
1 | void ObMultipleGetMerge::reset_with_fuse_row_cache() |
1 | void ObMultipleMerge::reset() |
ObStoreRowIterator 只有空的reuse()函数,没有reset()函数。
ObMemtableScanIterator和ObMemtableMGetIterator的reuse()函数都是调用了自己的reset()函数。
ObSEArray.reset()函数最终调用了ObSEArrayImpl.destroy()函数。
1 | template <typename T, int64_t LOCAL_ARRAY_SIZE, typename BlockAllocatorT, bool auto_free> |
更改后
1 | void ObMultipleGetMerge::reset_with_fuse_row_cache() |
1 | void ObMultipleMerge::reset() |
如果按照简单的sql语句:select /*+ordered use_nl(A,B)*/ count(*) from t1 A, v1 B where A.c1 = B.c1 and A.c2 = B.c2,在rescan阶段将会执行ObTableScanStoreRowIterator::rescan() -> ... ObMultipleScanMerge::reuse() -> ObMultipleMerge::reuse(),
1 | diff --git a/etc/observer.config.bin b/etc/observer.config.bin |
1 | diff --git a/src/storage/blocksstable/ob_block_sstable_struct.cpp b/src/storage/blocksstable/ob_block_sstable_struct.cpp |
1 | diff --git a/diff b/diff |
patch4的主要思想就是缓存了微块(rescan场景下缓存了微块,然后在ObSSTableRowIterator的reuse()中标记这是rescan场景);然后将read_hanldes_和micro_handles_重用了。
ObSSTableRowIterator等memtable或者sstable的iterator的reuse_try()函数是如何被调用的呢?因为每次rescan都会调用ObMultipleMerge的reuse()函数(其实它的子类的reuse()函数基本上也要调用这个函数),然后这个reuse()函数里面调用了ObMultipleMerge::reuse_iter_array()函数,然后我们修改了ObMultiMerge::reuse_iter_array()函数,使得它不是简单地reset所有iterator,而是reuse_try它们。
在ObSSTableRowIterator::reuse_try()函数中,我们取消reset read_handles_和reset micro_handles_ ,这样就可以再次用这两种handle了,并且在ObSimpleArray类中reset()函数中加入对所有元素的释放操作,这个操作比较重要。因为我们是通过stmt_allocator来给read_handles_和mico_handles_来申请空间,应该要找个合适的时机来释放这个空间。
我想我已经搞清楚了为什么使用HandleCache,也即在rescan场景下缓存微块,并且将read_handles_和micro_handles重用,可以提升性能。HandleCache是将这次scan右表需要的所有微块都缓存下来,micro_handles是将这些缓存的微块按照扫描顺序排好,micro_handles最里面有buf指针,指向微块数据,HandleCache也是的,这两个buf指针是相同的,也即HandleCache和micro_handles是对同一个微块数据的引用。当我们重用HandleCache和micro_handles时,下一次rescan就省了两部分工作:
1 | int reserve(common::ObArenaAllocator& allocator, const int64_t count) |
这是micro_handles和read_handles的空间分配函数,当我们不reset它们时,capacity_属性就不用清0,因此就不会走if路线了。

OB的block cache是微块级的,不建议缓存宏块。但是不懂是怎么通过range或者rowkey定位到宏块,又从宏块中找到对应的微块,反正最后是能够在一个rescan内,把所有微块给拿到内存中的。
OB的ObMicroBlockIndexHandle存储的是ObMicroBlockIndexCache,它是宏块中对微块的索引,通过它可以方便地在宏块中找到微块。
在ObHandleMgr::init()函数中,如果是is multi并且is ordered,说明这个不断传下来的rowkey或者range都是有序的,例如rowkey是1, 2, 4, 8, 20这样的,这样的话,我们就只需要对一个ObMicroBlockHandle进行缓存就好了,不需要HandleCache来缓存那么多的ObMicroBlockHandle。
1 | int ObExecuteResult::get_next_row(ObExecContext& ctx, const common::ObNewRow*& row) |
1 | ret = get_next_row(); |
我们可以看到get_next_row()函数应该是实际得到一行数据,结果保存在static_engine_root_.get_spec().output_中。
其实这里的static_engine_root_是ObOperator*类型的,是一个算子。
1 | int ObExecuteResult::get_next_row() |
1 | while (OB_SUCC(ret) && !got_row) { |
调用ObOperator::get_next_row()得到下一行数据,如果是OB_ITER_END,那就switch_iterator(),但是不明白这个函数是做什么的?
1 | int ObOperator::get_next_row() |
1 | if (OB_FAIL(startup_filter(filtered))) |
不懂startup_filter()函数是做什么的。当filtered为true时,表示过滤不成功。当还没有得到第一行记录的时候,记录一下当前的时间到op_monitor_info_中。
1 | int ObNestedLoopJoinOp::inner_get_next_row() |
1 | if (OB_UNLIKELY(LEFT_SEMI_JOIN == MY_SPEC.join_type_ || LEFT_ANTI_JOIN == MY_SPEC.join_type_)) { |
在构造ObNestLoopJoinOp的时候
1 | ObNestedLoopJoinOp::ObNestedLoopJoinOp(ObExecContext& exec_ctx, const ObOpSpec& spec, ObOpInput* input) |
join_row_with_semi_join()不知道干什么的?
在构造函数,state_初始化为JS_READ_LEFT。我们看到首先有一个while循环,所以肯定是在while循环中先read_left_operate(),然后再执行read_left_func_going(),在read_left_func_going()的最后,设置state_为JS_READ_RIGHT,然后就继续循环,读取右表中的一行read_right_operate(),然后就继续循环执行read_right_func_going(),这里会调用ObJoinOp::calc_other_conds()函数进行判断是否满足filter条件,如果满足,就设置output_row_produced_为true,这样就可以结束循环。所以这个循环的目的就是为了得到一行满足filter的数据,当然在上层,我们也进行了一个filter判断,我觉得这是多余的了。
这里state_是个很重要的属性,控制着NLJ的执行方向,是读左表,还是读右表,还是read_left_func_going(),具体这些执行怎么做的,后面具体说。
我们先看page_arena.h这个文件中是怎么定义和实现ObArenaAllocator这个类的。
1 | // class ObArenaAllocator final : public ObIAllocator |
1 | // template <typename CharT = char, class PageAllocatorT = DefaultPageAllocator> |
ObArenaAllocator每次是在一个page内进行内存分配的,如果在当前的page内可以分配,就分配,如果不能,就用ModulePageAllocator分配一个新的页,在这个页内进行分配。所有用ModulePageAllocator分配的页是用链表连接起来的。
1 | // template <typename CharT = char, class PageAllocatorT = DefaultPageAllocator> |
使用PageArena::extend_page()来扩展一个页。
1 | // template <typename CharT = char, class PageAllocatorT = DefaultPageAllocator> |
PageArena::alloc_new_page()调用了ModulePageAllocator::alloc()函数来分配一个内存页。这个alloc最终会调用到
1 | void* ObMallocAllocator::alloc(const int64_t size, const oceanbase::lib::ObMemAttr& attr) |
1 | } else if (OB_UNLIKELY(inner_attr.tenant_id_ >= PRESERVED_TENANT_COUNT)) { |
这里可以发现,我们要分配内存,就要使用allocator池中的一个allocator来分配,这里超过了我的知识范围,但是使用哪个allocator可能是和租户什么的有关。我看过一篇文章,解释过OB的租户的内存:OB 内存分配概述 (qq.com)
ObNestLoopJoinOp::read_left_operate() 函数调用 ObJoinOp::get_next_left_row()函数。
1 | int ObJoinOp::get_next_left_row() |
设置左表读出的row还没有join,然后调用left_的get_next_row(),它是ObTableScanOp动态类型。
1 | int ObTableScanOp::inner_get_next_row() { |
get_next_row_with_mode()成功后,就让output_row_count_增加。
1 | int ObTableScanIterator::get_next_row(common::ObNewRow*& row) { |
这里有个iter_和row_iter_属性,iter_是ObTableScanIterIterator对象,row_iter_是ObTableScanRangeArrayRowIterator对象。iter_可以产生多个row_iter_,当一个row_iter_遍历到结束后,就让iter_下一个row_iter_来继续遍历。结合多range和多rowkey的前提,可能是在ObTableScanIterator中多个ObTableScanRangeArrayRowIterator,它们由ObTableScanIterIterator来管理,并且和多range一一对应,比方说,根据主键来查表,where中主键的范围是key<7 或者 key > 100,那么这两个range就会对应两个ObTableScanRangeArrayRowIterator。(由于OB中的表是索引组织表,因此只要知道要查找的主键的范围,那么就可以根据索引轻松找到想要的元组记录在哪,这也是为什么OB有range的概念的原因,想一下,要是记录随便放在表文件中,有range有什么用呢,还是要一个页一个页地扫描)。
1 | int ObTableScanStoreRowIterator::get_next_row(ObStoreRow*& cur_row) { |
这个函数就调用了get_next_row()。main_iter_是ObQueryRowIterator类型,动态类型估计是ObMultipleScanMerge。
1 | int ObMultipleMerge::get_next_row(ObStoreRow*& row); |
这个get_next_row()函数很长,里面大致有这几个过程:
sql层的ObTableScanOp.cpp的inner_close()函数可以看到有table_allocator_的reuse()调用,这个时候就是把以前用allocator申请的空间给释放掉。
rescan的实现是在ObTableScanStoreRowIterator.cpp中的rescan()函数中可以看到,其实就是调用ObMultipleMerge::reuse_iter_array()把很多iterator给重用了,然后再调用open_iter()函数重新打开这些iterator,并没有涉及到数据的操作。
在ob_table_scan_op.cpp中的rt_rescan()函数中写了单机rescan的大致流程。
左表肯定是通过scan方式来得到数据,因为是要遍历左表的所有满足条件的行。
由于在我们的case中,右表是通过索引回表的方式得到数据的,因此是这样的流程:ObIndexMerge中先访问索引,得到每个rowkey,然后通过rowkey通过get的方式访问主表,来得到具体的行数据。这个流程实现在ObIndexMerge::get_next_row()函数中。
考虑src/storage/ob_handle_mgr.h/oceanbase::storage::ObHandleMgr::init()函数,它的部分调用链是:ObSSTableRowIterator::inner_open() -> ObSSTableRowIterator::init_handle_mgr() -> ObHandleMgr::init()。
我们可以发现这里面有一个HandleCache对象:
1 | } else if (is_multi) { |
“当is_multi为true时,说明有多个range或者rowkey传下来,这样就可以走这个分支,然后就可以使用这个HandleCache。我们考虑rescan的场景,每次rescan都会有新的range下来,而且这个range和上次rescan的range是连续的,因此就相当于is_multi为true的情况,如果这个时候我们不使用HandleCache,就是不够优化的。”
考虑src/ob_sstable_row_iterator.cpp/prefetch_block()函数。这个函数就是预取micro block的,但是为什么要预取呢,因为OB内部取磁盘数据是异步执行的,我们可以边读A微块边从磁盘取B微块,当A微块读取完成之后,说不定B微块就读好了。
ObHandleMgr和预取没有太大关系,只是我们在预取之后的数据之上,加了层Handle的cache。
我们是先从cache中找我们需要的微块,
1 | for (int64_t i = 0; OB_SUCC(ret) && i < sstable_micro_cnt; ++i) { |
如果没有找到,再从磁盘IO来找。因为每次找新的range或者rowkey会定位到一个微块,可能这个微块和上个读取微块相同,如果这时发现这个相同的微块在HandleCache中找到了,就很好。
1 | // ob_micro_block_handle_mgr.h |
1 | // ob_sstable_row_iterator.h |
1 | // ob_sstable_row_iterator.h |
rescan中的prefetch是怎么回事?
stmt_allocator申请的空间在何时释放的?
迭代器打开时做了什么事情?
ObTableScanOp的ObNewRowIterator result_属性在哪初始化的?
在ObTableScanOp::do_table_scan()函数中调用了
1 | if (OB_FAIL(das->table_scan(scan_param_, ab_iters_))) { |
这个das最终又调用了
1 | ObPartitionService::table_scan(ObVTableScanParam& vparam, common::ObNewRowIterator*& result) |
这里的result就是我们要的输出参数;然后又调用了
1 | ObPartitionStorage::table_scan(ObTableScanParam& param, const int64_t data_max_schema_version, common::ObNewRowIterator*& result) |
在这个函数里面有一条调用
1 | if (OB_UNLIKELY(NULL == (iter = rp_alloc(ObTableScanIterator, ObTableScanIterator::LABEL)))) { |
在什么层级下是线程安全的?
线程检查工具:https://www.jianshu.com/p/1f29ae9fceee
https://github.com/oceanbase/oceanbase/issues/488 这里面的issue,认领的话,在这里回复就好了
@王运来 来哥 发的机器不能访问外网吗?
export http_proxy=’http://172.16.0.232:8259‘
export https_proxy=$http_proxy
测试主机密码:6vqSJOonTr52LhzFmnUm
首发!OceanBase社区版入门教程开课啦!https://mp.weixin.qq.com/s/04YjSUsNoKtIRsC0OC394A

@nauta ob支持执行请求时打开trace_log,打开方式有两种,一种是通过hint中的trace_log字段,这种方式只对携带hint的当前语句生效;另一种是通过session变量ob_enable_trace_log,这种方式对这个session的后续所有语句生效。 打开trace_log后,通过show trace可以拿到上一次的trace_log,从中可以获取trace_id。同时show trace还可以看到这条请求大致的性能统计。 使用示例如下, # 语句级 select /+ trace_log=on /c1 from t1 limit 2; show trace; # session级 set ob_enable_trace_log = ‘ON’; select count(*) from t1; show trace; last_trace_id 使用select last_trace_id();可以查看上一条语句执行的trace_id,然后在日志中grep查找相关信息。
这里需要关注一个重点,LSM树(Log-Structured-Merge-Tree)正如它的名字一样,LSM树会将所有的数据插入、修改、删除等操作记录(注意是操作记录)保存在内存之中,当此类操作达到一定的数据量后,再批量地顺序写入到磁盘当中。LSM树的特点是:采用顺序写,提高了写的性能。
因此当MemTable达到一定大小flush到持久化存储变成SSTable后,在不同的SSTable中,可能存在相同Key的记录,当然最新的那条记录才是准确的。这样设计的虽然大大提高了写性能,但同时也会带来一些问题:
在Compact策略上,主要介绍两种基本策略:size-tiered和leveled。
一文带你了解LSM Compaction - 知乎 (zhihu.com)
随着数据写入不断增加,Minor Freeze不断触发,转储数据不断增多,一次查询可能需要越来越多的IO操作,读取延时也在不断变大。而执行Minor Compaction会使得转储文件数基本稳定,进而IO Seek次数会比较稳定,延迟就会稳定在一定范围。然而,Minor Compaction操作重写文件会带来很大的带宽压力以及短时间IO压力。因此可以认为,Minor Compaction就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。
OceanBase 存储引擎高级技术 - 知乎 (zhihu.com) OceanBase的LSM-tree
最容易理解的LSM树—以示例讲解合并查找过程_jinking01的专栏-CSDN博客 这里用图示讲解了LSM-tree的合并查找过程,里面LSM-tree的实现是,memtable用AVL树实现,sstable用B-tree实现。虽然不知道OceanBase的sstable的实现过程是什么样的,不管是B-tree还是排序好的元组序列,其实查询效率都挺高的,也都可以称为索引组织表,如果是B-tree,肯定是索引组织表,如果是排序序列,用二分查找也不错。
C++中Operator类型强制转换成员函数_chenglinhust的专栏-CSDN博客_operator 类型转换
C++将析构函数定义成virtual的真正原因_NEVER-CSDN博客_virtual 析构函数
用户显式调用析构函数的时候,只是单纯执行析构函数内的语句,不会释放对象对应的堆内存,摧毁对象
c++类继承中的using声明,派生类中用using声明改变基类成员的访问权限_lfw19891101的专栏-CSDN博客

这是我的简单的项目目录,针对它该如何写CMakeLists.txt呢。
1 | cmake_minimum_required(VERSION 3.20) |
因为我的.h文件都在源码下的src/目录里,所以就有代码include_directories(src)
因为有两个源文件hellow_world.cpp和main.cpp,而且想要生成的exe文件名称是test1,所以就有代码add_executable(test1 src/hellow_world.cpp src/main.cpp)
最后很多关于build和debug的文件都在cmake-build-debug文件夹里面。
Lex & Yacc 是用来生成词法分析器和语法分析器的工具。
Lex (A Lexical Analyzer Generator)用于生成词法分析器,用于把输入分割成一个个有意义的词块(称为token)。
Yacc(Yet Another Compiler-Compiler)用于生成语法解析器,用于确定上述分隔好的token之间的关联
lex文件内容分为三个段分别为:定义段、规则段、用户子程序段
三个段用 %% 进行分隔:
1 | /* 定义段 */ |
第1段是声明段,包括:
1-C代码部分:include头文件、函数、类型等声明,这些声明会原样拷到生成的.c文件中。
1 | %{ |
2-状态声明,如%x COMMENT。
1 | 状态声明将会在第二段用到,例如: |
3-正则式定义,如ID [A-Za-z_]+[A-Za-z0-9_]*。
1 | WS [\ \t\b\f] |

第2段是规则段,是lex文件的主体,包括每个规则是如何匹配的,以及匹配后要执行的C代码动作。
1 | {WS} /* ignore whitespace */; |
第3段是C函数定义段,如yywrap()的定义,这些C代码会原样拷到生成的.c文件中,该段内容可以为空。
1 | yywrap() |
1 | lex不仅返回相应的token,同时还会向yacc传递相应数据,如: |
yacc(Yet Another Compiler Compiler),是Unix/Linux上一个用来生成编译器的编译器(编译器代码生成器)。
使用巴克斯范式(BNF)定义语法,能处理上下文无关文法(context-free)。出现在每个产生式左边(left-hand side:lhs)的符号是非终端符号,出现在产生式右边(right-hand side:rhs)的符号有非终端符号和终端符号,但终端符号只出现在右端。
1 | 举个例子: |
yacc语法包括三部分:定义段、规则段和用户子例程段
1 | ...定义段... |
各部分由以两个百分号开头的行分开,尽管某一个部分可以为空,但是前两部分是必须的,第三部分和前面的百分号可以省略。
定义段:
1、C代码部分:include头文件、函数、类型等声明,这些声明会原样拷贝到生产的.c文件中
2、记号声明,如%token
3、类型声明,如%type
1 | %{ |
yyerror会对无法解析的语句进行处理
1 | //标识tokens |
规则段:
规则段由语法规则和包括C代码的动作组成。
规则中目标或非终端符放在左边,后跟一个冒号(:),然后是产生式的右边,之后是对应的动作(用{}包含)。
1 | %% |
以下面这个select语句解析为例
1 | select name,old from stu where old>10; |
e
最近一星期我就看了mini-ob的源代码,看明白了:
但是不懂的是:
没有看
想了解wal的代码相关,但是心态浮躁,看不下去代码。
只是练练leetcode,还需要做:
很久没有跑步,打球了。
最近花钱过于频繁,以后一个月要考虑如何省钱,多和敏在一起,但是不要kaif。
每天坚持记日记,基本没有间断过,坚持早起,三餐都和敏吃饭。
很久没有社交了,渐渐地不想关心华东师大和实验室的兄弟。
很久没有看情商类和心理健康类课程了。
很久没有看书了。
这篇文章参考了有哪些好的 LaTeX 编辑器?
在清华大学开源镜像站搜索CTAN(latex软件包),然后找到systems/texlive/Images目录,然后下载一个镜像。或者直接点击清华镜像texlive。
不得不说,这个镜像文件太大了,我以为latex就是一个轻量的东西。
下载完成之后,使用解压工具将这个iso文件解压到不含有中文的目录下面,然后安装时,也安装到不含有中文的目录下面,最好安装到D盘,因为它太大了。剩下的就按照安装程序走就好了,默认配置。
然后,将{安装目录}/bin/win32添加到系统PATH变量里面。
假设已经安装好了VS Code。
在VS Code侧边栏的extensions中搜索LaTex Workshop,然后安装这个插件。
在VS Code中按住快捷键CTRL+SHIFT+P,输入set,然后选择Preferences: Open Settings (JSON),然后将下面的代码粘贴到settings.json文件的最外面一对花括号里。
1 | 作者:黄盼 |
配置好了之后,就可以按CTRL+ALT+B编译latex文件了,按CTRL+ALT+V打开PDF文件。
这是一款 VSCode 的 LaTex 代码格式化插件。
由于我使用latex主要是为了写数学作业,所以要了解很多数学语法,参考typora: 一些常用数学符号。然后在VS Code中使用快捷键SHIFT+ALT+F就可以自动排版了。
在这里可以阅读这本latex入门书。但是这本书还是太长了,不适合快速入手。
参考在Latex中插入Python代码,需要下面的宏:
1 | \usepackage{graphicx} |
代码块
1 | \begin{python} |
这个宏有个好处就是直接代码高亮,解决了over-full的问题,非常好用。
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent:
meta: false
pages: false
posts:
title: true
date: true
path: true
text: false
raw: false
content: false
slug: false
updated: false
comments: false
link: false
permalink: false
excerpt: false
categories: false
tags: true