原文链接:https://developer.chrome.com/native-client/devguide/coding/3D-graphics

注意:已针对ChromeOS以外的平台公布了此处所述技术的弃用。
请访问我们的 迁移指南 了解详情。


3D图形

Native Client应用程序使用OpenGL ES 2.0 API进行3D渲染。本文档介绍如何在Native Client模块中调用OpenGL ES 2.0接口以及如何构建高效的呈现循环。它还解释了如何验证GPU驱动程序和测试特定的GPU功能,并提供了有助于确保渲染代码高效运行的提示。

注意:3D绘图和OpenGL是复杂的主题。本文档仅涉及与Native Client环境中的编程直接相关的问题。要了解有关OpenGL ES 2.0本身的更多信息,请参阅OpenGL ES 2.0编程指南。

验证客户端图形平台

Native Client是一种软件技术,它允许您对应用程序进行一次编码并在多个平台上运行,而无需担心每个可能的目标平台上的实现细节。在硬件级别提供相同的支持很困难。图形硬件来自许多不同的制造商,并由不同质量的驱动程序控制。特定的GPU驱动程序可能不支持每个OpenGL ES 2.0功能,并且已知某些驱动程序具有可被利用的漏洞。

即使GPU驱动程序可以安全使用,您的程序也应该在启动应用程序之前执行验证检查,以确保驱动程序支持您需要的所有功能。

用JavaScript审核驱动程序

在启动时,应用程序应执行一些可在其托管网页上以JavaScript实现的其他测试。执行这些测试的脚本应该包含在模块的embed标记之前,理想情况下,embed只有在这些测试成功时,标记才会出现在托管页面上。

首先要检查的是你是否可以创建图形上下文。如果可以,请使用上下文确认是否存在任何所需的OpenGL ES 2.0扩展。在检查扩展时,您可能需要引用扩展注册表并包含供应商前缀。

在Native Client中审核驱动程序

创建一个上下文

一旦您通过了JavaScript验证测试,就可以安全地将Native Client embed标记添加到托管网页并加载模块。作为模块初始化代码的一部分,您必须通过创建C ++ Graphics3D对象或调用PPB_Graphics3DAPI函数为应用程序创建图形上下文Create。不要以为这总会成功; 你仍然可能在创建上下文时遇到问题。如果您处于开发模式且无法创建上下文,请尝试创建更简单的版本,以查看是否要求不支持的功能或超出驱动程序资源限制。您的生产代码应始终检查上下文是否已创建,如果不是这样,则应正常失败。

检查扩展和功能

并非每个GPU都支持每个扩展或具有相同数量的纹理单元,顶点属性等。在启动时,调用glGetString(GL_EXTENSIONS)并检查扩展和所需的功能。例如:

  • 如果您使用mipmaps的非2次幂纹理,请确保 GL_OES_texture_npot存在。
  • 如果您使用浮点纹理,请确保GL_OES_texture_float 存在。
  • 如果使用的是DXT1,DXT3,DXT5或纹理,确保相应的扩展EXT_texture_compression_dxt1GL_CHROMIUM_texture_compression_dxt3以及 GL_CHROMIUM_texture_compression_dxt5存在的。
  • 如果您正在使用的功能glDrawArraysInstancedANGLE, glDrawElementsInstancedANGLEglVertexAttribDivisorANGLE,或PPAPI接口PPB_OpenGLES2InstancedArrays,确保相应的扩展GL_ANGLE_instanced_arrays存在。
  • 如果您正在使用该功能glRenderbufferStorageMultisampleEXT或PPAPI接口PPB_OpenGLES2FramebufferMultisample,请确保GL_CHROMIUM_framebuffer_multisample存在相应的扩展名。
  • 如果您正在使用的功能glGenQueriesEXTglDeleteQueriesEXT, glIsQueryEXTglBeginQueryEXTglEndQueryEXTglGetQueryivEXT, glGetQueryObjectuivEXT,或PPAPI接口PPB_OpenGLES2Query,确保相应的扩展GL_EXT_occlusion_query_boolean 存在。
  • 如果您正在使用的功能glMapBufferSubDataCHROMIUM, glUnmapBufferSubDataCHROMIUMglMapTexSubImage2DCHROMIUM, glUnmapTexSubImage2DCHROMIUM,或PPAPI接口PPB_OpenGLES2ChromiumMapSub,确保相应的扩展 GL_CHROMIUM_map_sub存在。

检查系统功能glGetIntegerv并相应地调整着色器程序以及纹理和顶点数据:

  • 如果在顶点着色器中使用纹理,请确保 glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, ...)glGetIntegerv(GL_MAX_TEXTURE_SIZE, ...)返回大于0的值。
  • 如果在单个着色器中使用的纹理超过8个,请确保 glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, ...)返回的值大于或等于所需的同时纹理数。

在Chrome网上应用店中审核驱动程序

如果您选择将应用程序放在Chrome Web Store中,则其Web Store 清单文件可以webgl 在requirements参数中包含该功能。它看起来像这样:

"requirements": {"3D": {"features": ["webgl"]}
}

虽然WebGL在技术上是一个JavaScript API,但指定该webgl功能也适用于OpenGL ES 2.0,因为两个接口都使用相同的驱动程序。

此清单项目不是必需的,但如果您将其包含在内,那么如果浏览器在不支持OpenGL ES 2.0或使用已知列入黑名单的GPU驱动程序的计算机上运行,​​Chrome网上应用店将阻止用户安装该应用程序可以邀请一次攻击。

如果Web Store确定用户的驱动程序不足,则应用程序将不会显示在商店的磁贴显示中。但是,它会出现在商店搜索结果中,或者如果用户直接链接到它,在这种情况下,用户仍然可以下载它。但是当用户到达安装页面时将检查清单要求,如果出现问题,浏览器将显示消息“此计算机不支持此应用程序。安装已被禁用。“

基于清单的检查仅适用于直接从Chrome网上应用店下载。通过内联安装加载应用程序时不会执行此操作。

遇到问题时该怎么办

使用上述审查程序,您应该能够在应用程序运行之前检测最常见的问题。如果存在问题,您的代码应尽可能清楚地描述问题。如果缺少功能,这很容易。无法创建图形上下文更难以诊断。至少,您可以建议用户尝试更新驱动程序。您可能希望转到描述如何进行更新的Chrome页面。

如果用户无法更新驱动程序,或者问题仍然存在,请务必收集有关其图形环境的信息。询问Chrome about:gpu页面的内容 。

记录不可靠的驱动程序

在用户文档中包含有关已知可疑驱动程序的信息会很有帮助。这可能有助于确定流氓驱动程序是否是问题的原因。GPU驱动程序黑名单有很多来源。可以在Chromium项目 和Khronos找到两个这样的列表。您可以使用这些列表在文档中包含警告用户有关危险驱动程序的信息。

测试你的防御

您可以通过使用以下标志运行Chrome(一次性全部)并观察应用程序如何响应来测试您的驱动程序验证代码:

  • --disable-webgl
  • --disable-pepper-3d
  • --disable_multisampling
  • --disable-accelerated-compositing
  • --disable-accelerated-2d-canvas

调用OpenGL ES 2.0命令

在Native Client中编写OpenGL ES 2.0调用有三种方法。

使用“纯”OpenGL ES 2.0函数调用

您可以通过Pepper扩展库进行OpenGL ES 2.0调用。SDK示例以examples/api/graphics_3d这种方式工作。在文件中 graphics_3d.cc,密钥初始化步骤如下:

  • 在文件顶部添加以下内容:

  • #include <GLES2/gl2.h>
    #include "ppapi/lib/gl/gles2/gl2ext_ppapi.h"

    定义功能InitGL。确切的规范attrib_list 将是特定于应用程序的。

  • bool InitGL(int32_t new_width, int32_t new_height) {if (!glInitializePPAPI(pp::Module::Get()->get_browser_interface())) {fprintf(stderr, "Unable to initialize GL PPAPI!\n");return false;}const int32_t attrib_list[] = {PP_GRAPHICS3DATTRIB_ALPHA_SIZE, 8,PP_GRAPHICS3DATTRIB_DEPTH_SIZE, 24,PP_GRAPHICS3DATTRIB_WIDTH, new_width,PP_GRAPHICS3DATTRIB_HEIGHT, new_height,PP_GRAPHICS3DATTRIB_NONE};context_ = pp::Graphics3D(this, attrib_list);if (!BindGraphics(context_)) {fprintf(stderr, "Unable to bind 3d context!\n");context_ = pp::Graphics3D();glSetCurrentContextPPAPI(0);return false;}glSetCurrentContextPPAPI(context_.pp_resource());return true;
    }

    包含逻辑Instance::DidChangeViewInitGL在必要时调用:在应用程序启动时(当图形上下文为NULL时)以及模块的View更改大小时。

使用Regal

如果您正在移植OpenGL ES 2.0应用程序,或者习惯使用OpenGL ES 2.0编写,那么您应该坚持使用上面描述的Pepper API或纯OpenGL ES 2.0调用。如果要移植使用不在OpenGL ES 2.0中的功能的应用程序,请考虑使用Regal。Regal是一个支持许多OpenGL版本的开源库。Regal最近添加了对Native Client的支持。Regal将大多数OpenGL调用直接转发到底层图形库,但它也可以模拟其他未包含的调用(当存在硬件支持时)。有关 详细信息,请参阅libregal。

使用Pepper API

您的代码可以直接调用Pepper PPB_OpenGLES2 API,就像任何Pepper接口一样。当您以这种方式编写时,每次调用OpenGL ES 2.0函数都必须以对Pepper接口的引用开始,第一个参数是图形上下文。要调用该函数glCompileShader,您的代码可能如下所示:

ppb_g3d_interface->CompileShader(graphicsContext, shader);

这种方法专门针对Pepper API。每个调用对应一个OpenGL ES 2.0函数,但语法对于Native Client是唯一的,因此源文件不可移植。

实现渲染循环

图形应用程序需要以高频率运行的连续帧渲染和重绘循环。要获得最佳帧速率,了解Native Client模块中的OpenGL ES 2.0代码如何与Chrome进行交互非常重要。

Chrome和Native Client流程

Chrome是一款多进程浏览器。每个Chrome标签都是一个单独的进程,运行具有自己主线程的应用程序(我们称之为Chrome主线程)。当应用程序启动Native Client模块时,该模块将在新的单独沙盒进程中运行。模块的进程有自己的主线程(Native Client线程)。Chrome和Native Client进程在其主线程上使用Pepper API调用相互通信。

当Chrome主线程调用Native Client线程(例如键盘和鼠标回调)时,Chrome主线程将阻止。这意味着Native Client线程上的冗长操作可以从Chrome中窃取周期,并且在Native Client线程上执行阻止操作可能会使您的应用程序停顿。

Native Client使用回调函数来同步两个进程的主线程。只有某些Pepper函数使用回调; SwapBuffers 就是其中之一。

SwapBuffers 及其回调函数

SwapBuffers是无阻碍的; 它从Native Client线程调用并立即返回。当SwapBuffers被调用时,它异步运行的Chrome的主线程上。它切换图形数据缓冲区,处理任何所需的合成操作,并重绘屏幕。屏幕更新完成后,SwapBuffer将从Chrome线程调用作为参数之一包含的回调函数,并在Native Client线程上执行。

要创建渲染循环,Native Client模块应该包含一个执行渲染工作然后执行的函数SwapBuffers,并将自身作为SwapBuffer回调传递。如果您的渲染代码高效且运行速度快,则此方案将实现最高的帧速率。该文档SwapBuffers解释了为什么这是最佳的:因为仅当插件的当前状态实际在屏幕上时才执行回调,此功能提供了一种速率限制动画的方法。通过在绘制下一帧之前等待图像在屏幕上,您可以确保不会比屏幕更新更快地生成更新。

下图说明了Chrome和Native Client进程之间的交互。特定于应用程序的呈现代码在DrawNative Client线程上调用的函数中运行。蓝色向下箭头阻止从主线程到Native Client的调用,绿色向上箭头是SwapBuffers从Native Client到主线程的非阻塞 调用。所有OpenGL ES 2.0调用都是Draw在Native Client线程中进行的。

/native-client/images/3d-graphics-render-loop.png

SDK示例 graphics_3d

SDK示例graphics_3d使用函数MainLoop(in hello_world.cc)创建如上所述的呈现循环。MainLoop 调用Render执行渲染工作,然后调用SwapBuffers,将自身作为回调传递。

void MainLoop(void* foo, int bar) {if (g_LoadCnt == 3) {InitProgram();g_LoadCnt++;}if (g_LoadCnt > 3) {Render();PP_CompletionCallback cc = PP_MakeCompletionCallback(MainLoop, 0);ppb_g3d_interface->SwapBuffers(g_context, cc);} else {PP_CompletionCallback cc = PP_MakeCompletionCallback(MainLoop, 0);ppb_core_interface->CallOnMainThread(0, cc, 0);}
}

管理OpenGL ES 2.0管道

OpenGL ES 2.0命令无法在Chrome或Native Client进程中运行。它们被传递到共享存储器中的FIFO队列,最好将其理解为GPU命令缓冲区。命令缓冲区由专用GPU进程共享。通过使用单独的GPU流程,Chrome实现了另一层运行时安全性,在将所有OpenGL ES 2.0命令及其参数发送到GPU之前对其进行审查。通过FIFO缓冲命令也可以加快代码速度,因为Native Client线程中的每个OpenGL ES 2.0调用都会​​立即返回,而处理可能会因GPU降低FIFO中排队的命令而延迟。

在更新屏幕之前,所有介入的OpenGL ES 2.0命令必须由GPU处理。程序员经常尝试通过在渲染代码中使用glFlushglFinish命令来确保这一点 。在Native Client的情况下,这通常是不必要的。该SwapBuffers命令执行隐式刷新,Chrome团队不断调整GPU代码以尽快使用OpenGL ES 2.0 FIFO。

有时3D应用程序可以以难以处理的方式写入FIFO。命令管道可能会填满,您的代码必须等待GPU刷新FIFO。如果是这种情况,您可以添加glFlush 调用以加速OpenGL ES 2.0命令FIFO的流程。在开始添加自己的刷新之前,首先尝试通过监视每帧的渲染时间并查找不一致落在同一OpenGL ES 2.0调用上的不规则尖峰来确定管道饱和度是否真的成为问题。如果您确信管道需要加速,请插入glFlush在启动不生成OpenGL ES 2.0命令的处理块之前调用代码。例如,在开始任何多线程粒子工作之前发出刷新,这样当您再次开始执行OpenGL ES 2.0调用时,命令缓冲区将会清除。确定呼叫的地点和频率glFlush可能很棘手,您需要尝试找到最佳点。

渲染和非活动标签

用户通常会在多标签浏览器中切换选项卡。执行3D渲染的性能良好的应用程序应暂停任何实时处理,并在其选项卡变为非活动状态时为其他进程产生循环。

在Chrome中,非活动选项卡将继续执行定时功能(例如 setIntervalsetTimeout),但定时器间隔将自动覆盖,并且在选项卡处于非活动状态时限制为不少于一秒。此外,SwapBuffers在选项卡再次处于活动状态之前,不会发送与呼叫关联的任何回叫。除了SwapBuffers选项卡处于非活动状态之外,您可能会从函数接收异步回调。根据应用程序的设计,您可以选择在它们到达时处理它们,或者将它们排入缓冲区并在选项卡变为活动状态时对它们进行处理。

标签处于非活动状态时经过的时间可能相当大。如果主线程脉冲基于SwapBuffers回调,则当选项卡处于非活动状态时,您的应用将不会更新。Native Client模块应该能够检测并响应其运行的选项卡的状态。例如,当选项卡变为非活动状态时,您可以在Native Client线程中设置一个原子标志,该标志将跳过3D渲染并SwapBuffers调用并继续每隔30毫秒左右调用主线程。这提供了时间来更新仍应在后台运行的功能,如音频。调用sched_yieldusleep在任何工作线程上释放资源并将循环停止到操作系统也可能会有所帮助 。

处理主线程中的选项卡激活

您可以在托管页面上使用JavaScript检测并响应激活或停用标签页。添加一个EventListener visibilitychange ,将消息发送到Native Client模块,如下例所示:

document.addEventListener('visibilitychange', function(){if (document.hidden) {// PostMessage to your Native Client moduledocument.nacl_module.postMessage('INACTIVE');} else {// PostMessage to your Native Client moduledocument.nacl_module.postMessage('ACTIVE');}}, false);

处理来自Native Client线程的选项卡激活

您还可以直接从Native Client模块检测并响应选项卡的激活或取消激活,方法是在函数中包含代码,pp::Instance::DidChangeView只要模块视图发生更改,就会调用该代码 。代码可以调用ppb::View::IsPageVisible以确定页面是否可见。不可见页面的最常见原因是页面位于后台选项卡中。

提示和最佳实践

以下是编写安全代码并使用Pepper 3D API获得最佳性能的一些建议。

这样做的

  • 确保启用attrib 0. OpenGL要求您启用attrib 0,但OpenGL ES 2.0不启用。例如,您可以定义具有2个属性的顶点着色器,编号如下:

    glBindAttribLocation(program, "positions", 1);
    glBindAttribLocation(program, "normals", 2);

    在这种情况下,着色器不使用attrib 0,如果Chrome在OpenGL上模拟OpenGL ES 2.0,Chrome可能必须执行一些额外的工作。即使您不使用attrib 0,启用attrib 0也总是更有效。

  • 检查着色器如何编译。着色器可以在不同系统上进行不同的编译,这可能导致glGetAttrib*函数返回不同的结果。每次重新编译着色器时,请确保顶点属性索引与相应的名称匹配。
  • 谨慎更新指数。出于安全原因,必须验证所有索引。如果更改索引,Native Client将再次验证它们。构建代码,以便不经常更新索引。
  • 使用较小的插件,让CSS缩放它。如果您遇到填充问题,通过CSS执行扩展可能会有所帮助。插件渲染的大小由<embed> 模块元素的width和height属性决定。网页上显示的实际大小由应用于元素的CSS样式控制。
  • 避免矩阵到矩阵的转换。对于某些版本的Mac OS,编译着色器时存在驱动程序问题。如果您遇到矩阵变换的编译器错误,请避免矩阵到矩阵的转换。例如,在通过mat4转换它之前,将vec3转换为vec4,而不是将mat4转换为mat3。

注意事项

  • 不要使用客户端缓冲区。OpenGL ES 2.0可以使用glVertexAttribPointer和使用客户端数据glDrawElements,但这确实很慢。尽量避免客户端缓冲区。请改用顶点缓冲区对象(VBO)。
  • 不要混合顶点数据和索引数据。默认情况下,Pepper 3D将缓冲区绑定到单个点。您可以创建一个缓冲区,并将其绑定到两个 GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFER,但是这将是昂贵的开销,所以不推荐。
  • 在渲染过程中不要调用glGet *或glCheck *。这是OpenGL程序的常规建议,但对于Chrome上的3D尤为重要。调用名称以这些字符串开头的任何OpenGL ES 2.0函数都会阻塞Native Client线程。这包括glGetError; 避免在发布版本中调用它。
  • 不要使用固定点(GL_FIXED)顶点属性。OpenGL ES 2.0不支持定点属性,因此在OpenGL ES 2.0中模拟它们的速度很慢。默认情况下,GL_FIXED在Pepper 3D API中关闭支持。
  • 不要从GPU读取数据。不要打电话glReadPixels,因为它很慢。
  • 不要更新大缓冲区的一小部分。在当前的OpenGL ES 2.0实现中,当您更新缓冲区的一部分( glSubBufferData例如)时,必须重新处理整个缓冲区。要避免此问题,请将静态和动态数据保存在不同的缓冲区中。
  • 不要调用glDisable(GL_TEXTURE_2D)。这是一个OpenGL ES 2.0错误。每次调用时,Chrome的about:gpu标签中都会显示错误消息 。

CC-By 3.0许可下提供的内容

编写你的应用程序(三)、3D图形相关推荐

  1. WPF,Silverlight与XAML读书笔记第三十九 - 可视化效果之3D图形

    原文:WPF,Silverlight与XAML读书笔记第三十九 - 可视化效果之3D图形 说明:本系列基本上是<WPF揭秘>的读书笔记.在结构安排与文章内容上参照<WPF揭秘> ...

  2. Java 编写程序打印以下图形_怎么用java编写如下程序在屏幕上输出如下图形 * *** *** * 循环语句做(if语句)...

    怎么用java编写如下程序在屏幕上输出如下图形 * *** ***** ******* ***** ... 4个答案  提问时间: 2011-12-16  22个赞 回答:这个图形对吧? * *** ...

  3. 用c语言编写程序输出* ***,用C语言如何编写程序输出以下图形

    用C语言如何编写程序输出以下图形 关注:169  答案:3  mip版 解决时间 2021-01-18 16:55 提问者傃顏莄蒾亾 2021-01-17 19:07 * * * * * * * * ...

  4. 怎样用才c语言定义一个三位数,怎样编写一个c语言程序计算任意输入一个3位数的整数的各位数字之和。要求主函数包括输入输出和调用该函数。...

    点击查看怎样编写一个c语言程序计算任意输入一个3位数的整数的各位数字之和.要求主函数包括输入输出和调用该函数.具体信息 答:#include void main(){ int a,sum=0; pri ...

  5. python3编写人工智能_人工智能学习第三章 编写第一个Python程序 及概念

    接下来我们将看见如何在 Python 中运行一个传统的"Hello World"程序. 本章将会教你如何编写.保存与运行 Python 程序. 通过 Python 来运行的你的程序 ...

  6. java 多线程 卖票_编写一个Java 多线程程序,完成三个售票窗口同时出售20张票(如下图所示);...

    编写一个Java 多线程程序,完成三个售票窗口同时出售20张票(如下图所示); 程序分析:(1)票数要使用同一个静态值: (2)为保证不会出现卖出同一个票数,要java多线程同步锁. 设计思路: (1 ...

  7. 驾考app驾考理论速成技巧驾校源码小程序驾考理论驾考科目三3D仿真灯光模拟实现

    在开发驾考科目一,科目四题库系统完成后,参考市面上已有的驾考系统,新开发驾考科目三3D仿真灯光模拟微信小程序端,整个系统预览图 系统前端UNIAPP开发,后端基于微擎核心开发,支持APP,公众号,微信 ...

  8. 编写一个C语言程序,输出如下图形(鱼) (5 分)

    编写一个C语言程序,输出如下图形(鱼) 输入格式: 无 输出格式: 答案: #include <stdio.h> int main() {     printf("       ...

  9. c++语言表白超炫图形_C++编写的表白小程序(图片围成爱心+烟花+音乐)-附源代码...

    一.效果 视频效果: C++编写的表白小程序(图片围成爱心+烟花+音乐)-附源码 二.项目完整源代码: 链接:https://pan.baidu.com/s/1zea3Wji1VN4FIrqXoa4L ...

最新文章

  1. jquery UI 后台图
  2. 蓝桥杯之--神秘三位数
  3. UMEditor调整文本编辑器的组件位置的方法
  4. python清空集合_python集合删除多种方法详解
  5. Django - 路由系统
  6. Bailian4003 十六进制转十进制【十六进制】
  7. 车道线定位及拟合:直方图确定车道线位置
  8. 通用量子计算机和容错量子计算,量子计算机研究(下册)——纠错和容错计算...
  9. python中文姓名排序_Python实现针对中文排序的方法
  10. 重力传感器、加速度传感器以及陀螺仪的区别
  11. 操作系统(2)复习 第八章 磁盘存储器的管理
  12. 豆瓣上征婚交友的小姐姐们
  13. map获取所有的key并返回列表
  14. MATLAB画三维动态魔方/旋转魔方/旋转立方体
  15. Swift中隐藏某一页面的返回按钮
  16. 孙悟空先后取过三次经,儒经、道经与佛经
  17. 看门狗喂狗被狗咬——窗口看门狗
  18. 如何将图像保存至计算机G7X,opencv之读入一幅图像,显示图像以及如何保存一副图像,基础操作...
  19. 浅谈 Node.js 热更新
  20. PDF能编辑吗,怎样去掉PDF上的水印

热门文章

  1. ALV字段目录lvc_s_fcat
  2. 22款奔驰S400L升级原厂主动氛围灯,H17钢琴条纹饰板等,浪漫奢华
  3. uni-app实现简单上传图片Demo(不考虑小程序,只实现网页和App)
  4. 互联网时代,传统企业如何做引流拓客?
  5. 2020年最好用的离线下载网盘,不限速度和空间
  6. kaggle上面的E-Commerce Data数据集练习(可视化与部分特征工程)
  7. 部署高校房屋管理系统可以实现哪些目标?
  8. 游戏盾是什么/为什么app会被攻击
  9. 第05课:Redis 实际应用中的异常场景及其根因分析和解决方案
  10. 邓仰东专栏|机器学习的那些事儿(一)