Apache IoTDB C# SDK 介绍

    最近今天写了IoTDB的三篇相关文章,完成了安装部署和客户端连接:

    • Windows Server上部署IoTDB 集群

    • DBeaver 连接IoTDBDriver

    • 将IoTDB注册为Windows服务

      TsFile 是 IoTDB 的底层数据文件,一种专门为时间序列数据设计的列式文件格式。IoTDB TsFile数据读写主要是下面两个结构:

      • IoTDB 提供了一个TSRecord工具,TSRecord记录了一个设备在一个时间戳下的若干测点信息。在c# 客户端里被抽象成了Row Record
      • IoTDB 提供了一个Tablet工具,Tablet记录了一个设备的多个测点的信息,按照一种表格的形式表示,这些测点具有相同的时间戳序列,因此可以应用在测点具有相同时间戳序列(每个时间戳下各个测点都具有值)的设备中。

      IoTDB C# SDK  叫做 Apache-IoTDB-Client-CSharp,Github:http://github.com/eedalong/Apache-IoTDB-Client-CSharp ,Nuget 包有两个:

      Apache.IoTDB和 Apache.IoTDB.Data。 其中 Apache.IoTDB.Data 是对ADO .NET支持,以.NET 读取数据库的方式方便不同使用习惯的用户, C#客户端也及时更新支持最新的Apache IoTDB的特性,如对齐序列插入、SchemaTemplate操纵接口的 支持、支持插入空值的Tablet结构等。

      最近刚刚发布了对IoTDB 1.0版本的支持的1.0.0.1预览版已经发布,欢迎各位试用并提issue~: http://www.nuget.org/packages/Apache.IoTDB/1.0.0.1-alpha


      使用示例


      // 参数定义
      string host = "localhost";
      int port = 6667;
      int pool_size = 2;

      // 初始化session
      var session_pool = new SessionPool(host, port, pool_size);

      // 开启session
      await session_pool.Open(false);

      // 创建时间序列
      await session_pool.CreateTimeSeries("root.test_group.test_device.ts1", TSDataType.TEXT, TSEncoding.PLAIN, Compressor.UNCOMPRESSED);
      await session_pool.CreateTimeSeries("root.test_group.test_device.ts2", TSDataType.BOOLEAN, TSEncoding.PLAIN, Compressor.UNCOMPRESSED);
      await session_pool.CreateTimeSeries("root.test_group.test_device.ts3", TSDataType.INT32, TSEncoding.PLAIN, Compressor.UNCOMPRESSED);

      // 插入record
      var measures = new List<string>{"ts1", "ts2", "ts3"};
      var values = new List<object> { "test_text", true, (int)123 };
      var timestamp = 1;
      var rowRecord = new RowRecord(timestamp, values, measures);
      await session_pool.InsertRecordAsync("root.test_group.test_device", rowRecord);

      // 插入Tablet
      var timestamp_lst = new List<long>{ timestamp + 1 };
      var value_lst = new List<object> {new() {"iotdb", true, (int) 12}};
      var tablet = new Tablet("root.test_group.test_device", measures, value_lst, timestamp_ls);
      await session_pool.InsertTabletAsync(tablet);

      // 关闭Session
      await session_pool.Close();


      详细接口信息可以参考接口文档

      连接池

      C#客户端暴露的所有接口均为异步接口。使用C#客户端从首先建立一个SessionPool开始,建立SessionPool时需要指定服务器的IP 、Port 以及 SessionPool的大小,SessionPool的大小代表本地与服务器建立的连接的数目。为了实现并发客户端请求,客户端提供了针对原生接口的连接池(SessionPool),由于SessionPool本身为Session的超集,当SessionPoolpool_size参数设置为1时,退化为原来的Session

      客户端 使用ConcurrentQueue数据结构封装了一个客户端队列,以维护与服务端的多个连接,当调用Open()接口时,会在该队列中创建指定个数的客户端,同时通过System.Threading.Monitor类实现对队列的同步访问。

      当请求发生时,会尝试从连接池中寻找一个空闲的客户端连接,如果没有空闲连接,那么程序将需要等待直到有空闲连接

      当一个连接被用完后,他会自动返回池中等待下次被使用

      在使用连接池后,客户端的并发性能提升明显,这篇文档展示了使用线程池比起单线程所带来的性能提升

      ByteBuffer

      在传入RPC接口参数时,需要对Record和Tablet两种数据结构进行序列化,我们主要通过封装的ByteBuffer类实现

      在封装字节序列的基础上,我们进行了内存预申请与内存倍增的优化,减少了序列化过程中内存的申请和释放,在一个拥有20000行的Tablet上进行序列化测试时,速度比起原生的数组动态增长具有35倍的性能加速,详见以下两篇文档:

      • ByteBuffer详细介绍
      • ByteBuffer性能测试文档

      在库里 有一个 IoTDB C#客⼾端性能分析报告:http://github.com/eedalong/Apache-IoTDB-Client-CSharp/blob/main/docs/time_profile_zh.pdf ,建议大家看一看,这里只说结论:

      • 在插⼊与该⽤⼾类似的结构化较强、没有空值、规整、每⾏的column固定的的数据时,建议使⽤insert_tablet接⼝,经过改善后的insert_tablet接⼝具备较好的性能,能满⾜该⽤⼾的需求
      • 数据量较⼤,但数据整体不规整或者有空值,每⾏数据的column数不定时建议使⽤insert_records接⼝,该接⼝对record数据的插⼊速度较为可观
      • 数据量⼩,需要对原有数据做出⼀定的修正 时,使⽤insert_record接⼝

       

      参考文章:

      • Apache IoTDB C#客户端介绍: http://github.com/eedalong/Apache-IoTDB-Client-CSharp/blob/main/docs/Apache%20IoTDB%20C%23%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BB%8B%E7%BB%8D%20(6).pdf
      • IoTDB C#客⼾端性能分析报告:http://github.com/eedalong/Apache-IoTDB-Client-CSharp/blob/main/docs/time_profile_zh.pdf 
      • API 接口: http://github.com/eedalong/Apache-IoTDB-Client-CSharp/blob/main/docs/API.md

      欢迎大家扫描下面二维码成为我的客户,扶你上云

      本文转载于网络 如有侵权请联系删除

      相关文章

      • Go安装OpenCV库(gocv)常见问题

        gocv是OpenCV4在Go中的绑定,使用它可以在Go里做图像处理。Windows安装官方介绍:https://gocv.io/getting-started/windows/其中gocv库提供的win_build_opencv.cmd为安装过程命令:下载opencv-4.5.0.zip并解压到C:\opencv下载opencv_contrib-4.5.0.zip并解压到C:\opencvcmakemingw32-makemingw32-makeinstall编译好后,添加C:\opencv\build\install\x64\mingw\bin到环境变量。常见问题:1.mingw32-make过程中断,编译失败可能是由于多线程编译时有的依赖库还没有生成造成的解决办法:反复执行mingw32-make2.Python的影响如果你的电脑装了Python,且环境变量添加了Python安装目录,可能会出现链接错误,这是因为有些lib依赖库按环境变量在Python目录中找到了,而这些库与编译不匹配。解决办法:环境变量去掉Python目录,更简单的办法是把Python目录改名,编译完成后再改回

      • mysql-使用存储过程一次性批量创建多张表

        mysql-使用存储过程一次性批量创建多张表强烈推介IDEA2020.2破解激活,IntelliJIDEA注册码,2020.2IDEA激活码DELIMITER$$ USE`DBName`$$ DROPPROCEDUREIFEXISTS`pro_TableCreate`$$ CREATEDEFINER=`root`@`%`PROCEDURE`pro_TableCreate`( ) BEGIN DECLAREiINT; DECLAREtable_nameVARCHAR(20); SETi=0; WHILEi<100DO #为了使表名成为xxx00这样的格式加的条件判断 IFi<10THEN SETtable_name=CONCAT('t_UserLog0',i); ELSE SETtable_name=CONCAT('t_UserLog',i); ENDIF; SET@csql=CONCAT( 'CREATETABLE',table_name,'( IDbigint(18)UNSIGNEDNOTNU

      • iNeuOS工业互联平台,生产过程业务联动控制

        1.概述 工业物联网也好、工业互联网也好或是其他生产系统,反向控制始终无法回避。搞工业最直接、最体现效果的两个方面是采集各种数据和生产过程业务控制,所谓大数据预测和分析,那是仁者见仁、智者见智,下一篇文章我们会专业来讨论工业“信息化”方面的问题。 控制主要涉及到单数据点手动控制、业务规则联动控制,不同行业、不同工艺场景,联动控制的复杂程度不一样,所以针对平台系统要能够支持不同场景控制需求的灵活脚本的能力。 场景案例简要描述:有三个原料罐,先对第一个原料罐进行加热,打开输出阀门,第一个原料罐没有料后关闭阀门,依次使用第二个和第三个原料罐,后续工艺控制过程省略……。      iNeuOS具体实现联动控制,框架示意如下图:2.平台演示 在线演示:http://www.ineuos.net/index.php/demo/demo-30.html(注:自已注册用户,体验系统功能)3.数据单点控制 主要用于开关量的控制和修改设备运行参数,如果通过iNeuView视图建模编辑控制操作,可以绑定单击事件、右键菜单选项等,命令方式:固定指令、自定义指令、开关指令。如下图:4.生产过程联动控制 联动控制

      • Laravel框架视图和模型操作方法分析

        本文实例讲述了Laravel框架视图和模型操作方法。分享给大家供大家参考,具体如下:视图简介:视图包含了应用程序渲染的HTML数据,并将应用程序的显示逻辑与控制逻辑有效的分离开。在Laravel中,视图被保存在resources/views目录中。//数组中的内容可以表示在视图中调用数组,可以用echo$name得到name的值 Route::get('/',function(){ returnview('greeting',['name'='James']); });复制视图可以被嵌套保存在resoureces/views目录的子目录中,”.”号或”\”被用来引用嵌套的视图。例如,可以通过下面语句引用resoureces/views/admin/profile.php这个视图:returnview('admin.profile',$data); returnviwe('admin/profile');复制创建默认视图,只需在文件名中加上xxx.blade.xxx判断

      • Kubernetes v1.17.0 正式发布

        k8sv1.17.0新增功能KubernetesVolumeSnapshot:功能现已在Kubernetesv1.17中处于beta版。它在Kubernetesv1.12中作为Alpha引入,第二个Alpha在Kubernetesv1.13中具有重大变化。 什么是VolumeSnapshot? 许多存储系统(如GoogleCloudPersistentDisks、AmazonElasticBlockStorage和许多本地存储系统)都可以创建PersistentVolume(持久卷)的“快照”。快照表示Volume的时间点副本,可用于设置新的Volume(预填充快照数据)或将现有Volume还原到先前状态(由快照表示)。 为什么要将VolumeSnapshot添加到K8s? KubernetesVolume插件系统提供强大的抽象功能,可以自动配置、附加和挂载块和文件存储。 这些功能都基于Kubernetes的工作负载可移植性:Kubernetes的目标是在分布式系统应用程序和底层集群之间创建一个抽象层,以便应用程序可以不知道底层集群的具体情况,且在部署时不需要“特定于集群”的知识。 K

      • 微信小程序使用公众号关注组件

        点击开启按钮,会提示选择需要展示的关注组件的公众号,这里以本站的官方微信为例子。在小程序中添加关注组件官方的WIKI很简单,如下所示:official-account 基础库2.3.0开始支持,低版本需做兼容处理。 用户扫码打开小程序时,开发者可在小程序内配置公众号关注组件,方便用户快捷关注公众号,可嵌套在原生组件内。复制Tips:使用组件前,需前往小程序后台,在“设置”->“接口设置”->“公众号关注组件”中设置要展示的公众号。注:设置的公众号需与小程序主体一致。 在一个小程序的生命周期内,只有从以下场景进入小程序,才具有展示引导关注公众号组件的能力: 当小程序从扫二维码场景(场景值1011)打开时 当小程序从扫小程序码场景(场景值1047)打开时 当小程序从聊天顶部场景(场景值1089)中的“最近使用”内打开时,若小程序之前未被销毁,则该组件保持上一次打开小程序时的状态 当从其他小程序返回小程序(场景值1038)时,若小程序之前未被销毁,则该组件保持上一次打开小程序时的状态 每个页面只能配置一个该组件。复制示例:<official-account><

      • 性能测试常见指标介绍

        1注册用户数    注册用户数指软件中已经注册的用户,这些用户是系统的潜在用户,随时都有可能上线。这个指标的意义在于让测试工程师了解系统数据中的数据总量和系统最大可能有多少用户同时在线。2在线用户数     在线用户数是指某一时刻已经登录系统的用户数量。在线用户数只是统计了登录系统的用户数量,这些用户不一定都对系统进行操作,对服务器产生压力。3并发用户数    不同于在线用户数,并发用户数是指某一时刻向服务器发送请求的在线用户数,他是衡量服务器并发容量和同步协调能力的重要指标,从这个含义上讲,我们可能会如下两种理解:  同一时刻向服务器发送相同或者不同请求的用户数,也就是说,既可以包括对某一业务的相同请求,也可以包括对多个业务的不同请求 同一时刻向服务器发送相同请求的用户数,仅限于某一业务的相同请求4请求的响应时间     响应时间就是用户感受软件系统为其服务所消耗的时间。对于web系统,请求的响应时间指的是从客户端发起的一个请求时间,到客户端接收到从服务器返回的响应结束。(1)在3秒之内,页面给予用户响应所有显示,可认为是很不错的(2)在3-5秒之内,页面给予用户响应所有显示,可认为

      • 年度热门编程语言排行榜,你擅长的语言排第几

        来源:开源中国社区 原文标题:Topprogramminglanguagesthatwillbemostpopularin2017 原文网址:http://blog.hackerearth.com/2016/11/top-programming-language-2017.html想知道全球最受欢迎的编程语言是什么吗?它们的判断标准又是怎样的呢?我们都知道,C++,MATLAB,Java一直都受到技术学院的青睐,大多数毕业生都热衷于学习这些语言。但它们是否是业界所需要的呢?抱着这个疑问,我们访问了几个可信度较高的语言索引网站,同时还深入到Indeed和Glassdoor等全球门户网站,试图收集数据,以总结出全球最受欢迎的语言是哪些,以及行业内最需要的语言是什么。注:对编程语言进行受欢迎度评选,并不是为了证明哪项语言好,哪项语言不好,而是希望能通过这一类分析,找出用户最喜欢以及业界最需要的语言。TIOBEIndexTIOBE编程社区索引由荷兰Eindhoven的TIOBE公司创立和维护。TIOBE代表着“真诚的重要性”,该索引将每项语言作为关键字,按照搜索引擎的查询数量对语言进行排名。因

      • nginx简易防CC策略规则

        1.根据访问地址过滤。检测到访问地址有test=这些关键词,自动跳转到公安备案网。if ($request_uri ~* test=) { return 301 http://www.beian.gov.cn; }复制2.根据访问地址过滤。检测到来源地址有Baiduspider,自动跳转到公安备案网。 有的版权狗软件,顺序都搞不清楚,改改就拿来用了!if ($http_referer ~* Baiduspider) { return 301 http://www.beian.gov.cn; }复制3.根据ua过滤检测到ua,有Baiduspider直接过滤。反正被打也会打不开,跟过滤蜘蛛一个性质!if ($http_user_agent ~* Baiduspider) { return 444; }复制4.检测代理IP检测代理IP是否设置if ($proxy_add_x_forwarded_for != $remote_addr){ return 444; }复制

      • 卷积神经网络基础;leNet;卷积神经网络进阶(1天)

        卷积神经网络(CNN)由输入层、卷积层、激活函数、池化层、全连接层组成,即INPUT(输入层)-CONV(卷积层)-RELU(激活函数)-POOL(池化层)-FC(全连接层) 卷积层 用它来进行特征提取,如下:    输入图像是32*32*3,3是它的深度(即R、G、B),卷积层是一个5*5*3的filter(感受野),这里注意:感受野的深度必须和输入图像的深度相同。通过一个filter与输入图像的卷积可以得到一个28*28*1的特征图,上图是用了两个filter得到了两个特征图; 我们通常会使用多层卷积层来得到更深层次的特征图。如下:      关于卷积的过程图解如下:   输入图像和filter的对应位置元素相乘再求和,最后再加上b,得到特征图。如图中所示,filterw0的第一层深度和输入图像的蓝色方框中对应元素相乘再求和得到0,其他两个深度得到2,0,则有0+2+0+1=3即图中右边特征图的第一个元素3.,卷积过后输入图像的蓝色方框再滑动,stride(步长)=2,如下:   如上图,完成卷积,得

      • 2020~2021 学年初三上学期十二月月考总结与展望

        先大概扯一下这次考试的概况吧。上次从班主任那里拿走了五份礼物,当时我说,我下次还要拿更多的!现在看看,纯属扯淡。 数学似乎没有什么长进。排名往前进了一名,变成了第七名。这次被我的垃圾计算能力坑惨了,倒一算错了,倒三数据代错了,倒四也算错了……另外我从小练就的精准尺规作图终于派上用场了——但不是作图题——而是填空题的最后一题——通过“高精度作图与测量”,我用尺子量出了正确答案……不过真的,考试考完再去做那道题,认真想想也不会非常难(我没看林翰飞的答案,我研究出了一种更麻烦的新方法,然后简化完思路发现本质和他的方法是一样的——三角函数和相似本质并没有什么不同)。不过每次考试,只要是理科,都有这种考试的时候觉得很难、考完再去做却觉得很简单的感觉,可能是考试的时候太紧张了吧。 英语这次真的让我非常郁闷。英语本来是我长期的优势科目,是救火队的角色,本来一直在年段A1的啊……但这次居然掉到了A3……而且上一次我英语是班一,这次直接退到第8名左右……这下连大本营都烧起来了,还灭什么火啊……我想了一下原因,应该是之前张老师说,觉得自己英语比较好的、晚上作业做到很迟的人,英语那个橙皮本(同步练习+过关测

      • aspectj 表达式 execution切点函数

        execution函数用于匹配方法执行的连接点,语法为: execution(方法修饰符(可选) 返回类型 方法名(参数)异常模式(可选)) 参数部分允许使用通配符: * 匹配任意字符,但只能匹配一个元素 ..匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用 + 必须跟在类名后面,如Superman+,表示类本身和继承或扩展指定类的所有类   示例中的*run(..)解读为: 方法修饰符 无 返回类型   *匹配任意数量字符,表示返回类型不限 方法名     run表示匹配名称为run的方法 参数       (..)表示匹配任意数量和类型的输入参数 异常模式   不限 例如1: 定义切入点表达式 execution(*com.demo.service.impl..*.*(..))  1、execution():表达

      • uboot1: 启动流程和移植框架

        目录0环境1移植框架3执行流程3.0链接地址3.1start.S,入口3.2__main3.3board_init_f()和init_sequence_f[]3.4relocate3.5board_init_r()3.5.1init_sequence_r3.5.2main_loop参考 0环境 ARMV8,uboot2020.10,rpi3平台 1移植框架 board,不用说了,板级,uboot使用dts后,这块代码应尽量简化 machine,SOC级,主要是一些外设 ARCH,如arm(包含armv7和armv8) CPU,如armv8 各框架启动时的关系: 配置相关文件: configs/xxx_defconfig:平台的默认配置,make***_defconfig时会用到 include/conigs/***.h:各平台的一些额外CONFIG_配置项,写在头文件里 复制 3执行流程 3.0链接地址 Makefile中: LDFLAGS_u-boot+=-Ttext$(CONFIG_SYS_TEXT_BASE) 复制 链接文件 ENTRY(_start) SECTIONS

      • 【51nod 1824】染色游戏

        题目 有n个红球,m个蓝球,从中取出x个红球和y个蓝球排成一排的得分是rx⋅by,其中r0=b0=1。 定义f(t)表示恰好取出t个球排成一排的所有可能局面的得分之和。 两个局面相同,当且仅当这两排球的个数相等,且在对应列位置上的颜色都是相同的。 小Q想知道,有多少t(1≤t≤n+m)使得f(t)是奇数,你能告诉他满足条件的t2之和吗? 对于样例,f(1)=2,f(2)=5,f(3)=13,f(4)=28,f(5)=50,f(6)=60,答案是$22+32=13$。 分析 cty爆音通道to分治做法什么的看到我一脸懵逼 于是只能打个FWT 题目中的\(f(t)=\sum_{x+y=t}r_xb_yC_{t}^{x}\),这个不用多解释。 然后考虑如何判断\(f(t)\)是否为奇数, 因为只用判断奇偶,只用保留%2的结果。 据说根据lucas定理得出,\(C_{n+m}^n\)为奇数,尤其尤其仅当\([x\and\y=0]\) 于是 原式得 \[f(t)=\sum_{x+y=t}r_xb_y[x\and\y=0] \]\[=\sum_{x+y=t}r_xb_y[x\or\y=t] \]\

      • controller和RequestMapping

        一、控制器定义控制器提供访问应用程序的行为,通常通过服务接口定义或注解定义两种方法实现。控制器解析用户的请求并将其转换为一个模型。在SpringMVC中一个控制器可以包含多个Action(动作、方法)。 使用注解@Controller定义控制器org.springframework.stereotype.Controller注解类型用于声明Spring类的实例是一个控制器; Spring可以使用扫描机制来找到应用程序中所有基于注解的控制器类,为了保证Spring能找到你的控制器,需要在配置文件中声明组件扫描。 @Controller@RequestMapping("/foo")publicclassFooController{//@RequestMapping("/action1")@RequestMapping({"/","action1","111"})publicStringaction1(Modelmodel){model.addAttribute("message","测试action1");return"foo/index";}}二、@RequestMapping详解@Re

      • windows下安装mysql

        之前在linux上装过mysql,今天想在windows下安装mysql   1、mysql下载,打开官网,下载自己所需要的mysql 官网:https://dev.mysql.com/downloads/mysql/  然后下载对应的版本       下载后,解压,我是放在D盘上   2、然后配置环境变量,在系统变量中的path下添加:D:\mysql\mysql-8.0.17-winx64\bin     3、需要配置点东西,重点来了, 需要在D:\mysql\mysql-8.0.17-winx64创建一个data的空文件夹 在创建一个my.ini的文件,放在bin目录下,内容为   [mysql]#设置mysql客户端默认字符集default-character-set=utf8[mysqld]#设置3306端口port=3306#设置mysql的安装目录basedir=D:/mysql/mysql-8.0.17-winx64#设置mysql数据库的数据的存放目录da

      • 【原生】Cocos3.x 发布原生(Android4.1、模拟器乱码、原生通讯、内存泄漏检测系统)

        版本:3.5.2 参考: cocos文档-打包发布到原生 cocos文档-安装配置原生环境 cocos文档-原生平台JavaScript调试 cocos文档-热更新范例教程 cocos文档-热更新管理器   大部分设置都参考2.x的文章。 【原生】CocosCreator原生开发环境配置(JavaSDK,AndroidStudio,Python,豌豆荚,真机实测、屏幕刷新率改变游戏FPS) 【原生】CocosCreatorapk打包发布(APPABI、jsc、网易易盾、权限、app名称图标、包名、签名) 【原生】CocosCreatorAndroid和游戏的通讯(Java和TS互相调用、传递JSON数据、监听返回键) 【原生】CocosCreator原生热更新(demo源码、动态热更、强更新) 【原生】CocosCreator原生调试(真机、模拟器、profile、手机IP)   3.x下测试的几点问题。   一AndroidStudio版本必须4.1及以上版本 AndroidStudio下载地址 cocos3.x必须使用Android4.1以及以上版本,

      • 第三方库安装失败

        https://blog.csdn.net/weixin_40431584/article/details/103914584

      • 几种工具反编译被编译好的DLL文件

          分享一个连接:  http://www.cr173.com/html/22369_1.html

      • 1元、5元、10元、20元、50元、100元六种面额,输入N,计算有多少种组合可以等于N

        有足够多的1元、5元、10元、20元、50元、100元六种面额的纸币,输入N,计算出有多少种组合可以等于N; 如输入N=5,则返回2。因为有两种组合:1+1+1+1+1=5,5=5,即5张1元或者1张5元; Java代码如下: publicclassTest{ publicintgetGroups(intN){ intround5=(int)Math.floor(N/5);//N除以5并向下取整 intround10=(int)Math.floor(N/10); intround20=(int)Math.floor(N/20); intround50=(int)Math.floor(N/50); intround100=(int)Math.floor(N/100); intnum=0; if(N>0&&N<5){//N取值大于0小于5 num=1; } if(N>=5&&N<10&&round5>0){//N取值大于等于5小于10 for(inti5=0;i5<=round

      • js类笔记+

        //baseconsole//classPerson{//say(msg){//console.log(msg)//}//}////letapp=newPerson();//app.say('hello12');复制

      相关推荐

      推荐阅读