zoukankan      html  css  js  c++  java
  • MongoDB分析工具之二:MongoDB分析器Profile

    MongoDB优化器profile

    在MySQL 中,慢查询日志是经常作为我们优化数据库的依据,那在MongoDB 中是否有类似的功能呢?答案是肯定的,那就是MongoDB Database Profiler。Profiler默认是关闭的,你可以选择全部开启,或者有慢查询的时候开启。

    1.开启profiling功能

    有两种方式可以控制 Profiling 的开关和级别,第一种是直接在启动参数里直接进行设置。启动MongoDB 时加上–profile=级别 即可。也可以在客户端调用db.setProfilingLevel(级别) 命令来实时配置,Profiler 信息保存在system.profile 中。我们可以通过db.getProfilingLevel()命令来获取当前的Profile 级别,类似如下操作:

    db.setProfilingLevel(2);

    上面profile 的级别可以取0,1,2 三个值,他们表示的意义如下:

    0 – 不开启

    1 – 记录慢命令 (默认为>100ms)

    2 – 记录所有命令

    Profile 记录在级别1 时会记录慢命令,那么这个慢的定义是什么?上面我们说到其默认为100ms,当然有默认就有设置,其设置方法和级别一样有两种,一种是通过添加 –slowms 启动参数配置。第二种是调用db.setProfilingLevel 时加上第二个参数:

    db.setProfilingLevel( level , slowms )
    db.setProfilingLevel( 1 , 10 );
    db.setProfilingLevel(1, { slowms: 20 })

    2.查看 Profile的设置级别

    db.getProfilingLevel()

    > db.setProfilingLevel(1)
    { "was" : 2, "slowms" : 100, "sampleRate" : 1, "ok" : 1 }
    > db.getProfilingLevel()
    1
    >

    3.查询 Profiling 记录

    与MySQL的慢查询日志不同,Mongo Profile 记录是直接存在系统db里的,记录位置 system.profile ,所以,我们只要查询这个Collection的记录就可以获取到我们的 Profile 记录了。

    慢查询(system.profile)说明

    通过下面的例子说明,更多信息见:http://docs.mongodb.org/manual/reference/database-profiler/

    currentOp和数据库分析器为所有CRUD操作报告相同的基本诊断信息,包括以下内容:

    这些操作还包括在慢速查询的日志记录中(有关慢速查询日志记录的更多信息,请参阅slowOpThresholdMs)。

    示例:

    {
       "op" : "query",
       "ns" : "test.report",
       "command" : {
          "find" : "report",
          "filter" : { "a" : { "$lte" : 500 } },
          "lsid" : {
             "id" : UUID("5ccd5b81-b023-41f3-8959-bf99ed696ce9")
          },
          "$db" : "test"
       },
       "cursorid" : 33629063128,
       "keysExamined" : 101,
       "docsExamined" : 101,
       "numYield" : 2,
       "nreturned" : 101,
       "queryHash" : "811451DD",
       "planCacheKey" : "759981BA",
       "locks" : {
          "Global" : {
             "acquireCount" : {
                "r" : NumberLong(3)
             }
          },
          "Database" : {
             "acquireCount" : { "r" : NumberLong(1) },
             "acquireWaitCount" : { "r" : NumberLong(1) },
             "timeAcquiringMicros" : { "r" : NumberLong(69130694) }
          },
          "Collection" : {
             "acquireCount" : { "r" : NumberLong(1) }
          }
       },
       "storage" : {
          "data" : {
             "bytesRead" : NumberLong(14736),
             "timeReadingMicros" : NumberLong(17)
          }
       },
       "responseLength" : 1305014,
       "protocol" : "op_msg",
       "millis" : 69132,
       "planSummary" : "IXSCAN { a: 1, _id: -1 }",
       "execStats" : {
          "stage" : "FETCH",
          "nReturned" : 101,
          "executionTimeMillisEstimate" : 0,
          "works" : 101,
          "advanced" : 101,
          "needTime" : 0,
          "needYield" : 0,
          "saveState" : 3,
          "restoreState" : 2,
          "isEOF" : 0,
          "invalidates" : 0,
          "docsExamined" : 101,
          "alreadyHasObj" : 0,
          "inputStage" : {
             ...
          }
       },
       "ts" : ISODate("2019-01-14T16:57:33.450Z"),
       "client" : "127.0.0.1",
       "appName" : "MongoDB Shell",
       "allUsers" : [
          {
             "user" : "someuser",
             "db" : "admin"
          }
       ],
       "user" : "someuser@admin"
    }

    system.profile.op:操作的类型。可能的值是:

    • command
    • count
    • distinct
    • geoNear
    • getMore
    • group
    • insert
    • mapReduce
    • query
    • remove
    • update

    system.profile.ns:操作目标的命名空间。MongoDB中的名称空间采用数据库的形式,后跟一个点(.),然后是集合的名称。
    system.profile.command:在3.6版中进行了更改。包含与此操作关联的完整命令对象的文档。如果命令文件超过50kb,则文件的格式如下:

    "command" : {
      "$truncated": <string>,
      "comment": <string>
    }

    $truncated字段包含文档的字符串摘要,如果存在,则不包括文档的注释字段。如果摘要仍然超过50 kb,则进一步截断它,用字符串末尾的省略号(…)表示。
    如果将comment传递给operation,则会显示comment字段。
    下面的示例输出包含一个command 对象,用于在一个名为test的数据库中对一个集合的命名项执行查找操作:

    "command" : {
      "find" : "items",
      "filter" : {
        "sku" : 1403978
      },
      "$db" : "test"
    }

    下面的示例输出包含用于getMore操作的命令对象,getMore操作是由一个命令生成的,该命令的游标id为80336119321,用于一个名为test的数据库中的集合命名项:
        "command" : {
          "getMore" : NumberLong("80336119321"),
          "collection" : "items",
          "$db" : "test"
        }

    system.profile.originatingCommand:版本3.6中的更改:对于从游标检索下一批结果的“getmore”操作,originatingCommand字段包含最初创建该游标的完整命令对象(例如find或aggregate)。

    system.profile.cursorid:查询和getmore操作访问的游标的ID。
    system.profile.keysExamined:在3.2.0版本中更改。从system.profile.nscanned重命名。MongoDB为了执行操作而扫描的索引键的数量。一般来说,如果键值检测比nreturn高得多,数据库就会扫描许多索引键来查找结果文档。考虑创建或调整索引来提高查询性能。在3.4版本中进行了更改。
    keysexamination可用于以下命令和操作:

    system.profile.docsExamined:在3.2.0版本中更改:从system.profile.nscannedObjects重命名。MongoDB为了执行操作而扫描的集合中的文档数量。在3.4版本中进行了更改。
    docschecked可用于以下命令和操作:

    system.profile.moved:在3.4删除。版本3.0.0中的更改:仅在使用MMAPv1存储引擎时出现。
    当更新操作将一个或多个文档移动到磁盘上的新位置时,此字段的值为true。如果操作没有导致移动,则不显示此字段。导致移动的操作要比就地更新花费更多的时间,通常是文档增长的结果。
    system.profile.nmoved:版本3.0.0中的更改:仅在使用MMAPv1存储引擎时出现。
    操作在磁盘上移动的文档数量。只有在操作导致移动时才会出现此字段。字段的隐式值为零,只有当非零时才会出现字段。
    system.profile.hasSortStage:在3.2.0版本中更改:从system.profile.scanAndOrder重新命名。
    hasSortStage是一个布尔值,当查询不能使用索引中的顺序返回请求的排序结果时,该布尔值为真;例如,MongoDB必须在从游标接收到文档后对文档进行排序。只有当值为真时才会出现字段。
    在3.4版本中进行了更改。
    hasSortStage可用于以下命令和操作:

    system.profile.ndeleted:操作删除的文档数量。
    system.profile.ninserted:由操作插入的文档数。
    system.profile.nMatched:新版本2.6。与system.profile匹配的文档数量。更新操作的查询条件。
    system.profile.nModified:新版本2.6。更新操作修改的文档数量。
    system.profile.upsert:一个布尔值,指示更新操作的upsert选项值。只有upsert为真时才会出现。
    system.profile.fastmodinsert:从3.4版开始不推荐。记录符合下列所有条件的更新操作数目:
    是upserts(导致插入)
    不要使用诸如$set之类的修饰符操作

    system.profile.fromMultiPlanner:新版本3.2.5。一个布尔值,指示查询规划器在为查询选择获胜执行计划之前是否评估多个计划。只有当价值为真时才会出现。
    system.profile.replanned:新版本3.2.5。一个布尔值,指示查询系统是否删除缓存的计划并重新评估所有候选计划。只有当价值为真时才会出现。
    system.profile.keysInserted:为给定的写操作插入索引键的数目。
    system.profile.keysDeleted:在3.4删除。更新在操作中更改的索引键的数目。更改索引键会带来较小的性能代价,因为数据库必须删除旧键并将新键插入到B-tree索引中。
    system.profile.writeConflicts:新版本3.0.0。写操作中遇到冲突的次数;例如,一个更新操作试图修改与另一个更新操作相同的文档。参见“写入冲突”。

    system.profile.numYield:允许其他操作完成的操作所产生的次数。通常,当需要访问MongoDB尚未完全读入内存的数据时,操作会产生收益。这允许在MongoDB为生成操作读入数据时完成内存中有数据的其他操作。有关更多信息,请参阅有关操作何时产生的常见问题。
    system.profile.locks:新版本3.0.0:locks替换了lockStats字段。
    system.profile.locks提供操作期间持有的各种锁类型和锁模式的信息。
    可能的锁类型有:

    锁类型      描述
    Global      表示全局锁。
    MMAPV1Journal  表示用于同步日志写入的MMAPv1存储引擎特定锁;对于非mmapv1存储引擎,MMAPV1Journal的模式为空。
    Database      表示数据库锁。
    Collection    表示集合锁。
    Metadata      表示元数据锁。
    oplog        表示oplog上的锁。
    可能的模式如下:
    锁定模式      描述
    R          表示共享锁。
    W          表示排他(X)锁。
    r          表示共享的意图(IS)锁。
    w          表示意图独占(IX)锁。

    返回的各种锁类型的锁信息包括:
    system.profile.locks.acquireCount:操作以指定模式获取锁的次数。
    system.profile.locks.acquireWaitCount:操作必须等待acquirecallock获取的次数,因为锁处于冲突模式。acquireWaitCount小于或等于acquirecore。
    system.profile.locks.timeAcquiringMicros:操作必须等待获取锁的累积时间(以微秒为单位)。
    时间获取微s除以acquireWaitCount给出了特定锁定模式的平均等待时间。
    system.profile.locks.deadlockCount:在等待锁获取时,操作遇到死锁的次数。
    有关锁模式的更多信息,请参见MongoDB使用哪种类型的锁。

    system.profile.storage:4.0.9新版本。
    system.profile.storage信息提供关于存储引擎数据和操作等待时间的度量。
    只有当值大于零时才返回特定的存储指标。
    system.profile.storage.data.bytesRead:4.0.9新版本。操作从磁盘读取到缓存的字节数。
    system.profile.storage.data.timeReadingMicros:4.0.9新版本。从磁盘读取操作所需的时间(以微秒为单位)。
    system.profile.storage.data.bytesWritten:4.0.9新版本。操作从缓存写到磁盘的字节数。
    system.profile.storage.data.timeWritingMicros:4.0.9新版本。写入磁盘所需的时间(以微秒为单位)。
    system.profile.storage.timeWaitingMicros.cache:4.0.9新版本。操作必须等待缓存中的空间的时间(以微秒为单位)。
    system.profile.storage.timeWaitingMicros.schemaLock:4.0.9新版本。操作(如果修改模式)必须等待获取模式锁的时间(以微秒为单位)。
    system.profile.storage.timeWaitingMicros.handleLock:4.0.9新版本。操作必须等待以微秒为单位获取所需数据句柄上的a锁的时间。
    system.profile.nreturned:操作返回的文档数量。
    system.profile.responseLength:操作结果文档的长度(以字节为单位)。较大的响应量会影响性能。要限制查询操作的结果文档的大小,可以使用以下任何一种方法:

    请注意
    当MongoDB将query profile信息写入日志时,responseLength值位于名为reslen的字段中。

    system.profile.protocol:MongoDB有线协议请求消息格式。
    system.profile.millis:从mongod的角度来看,从操作开始到操作结束的时间以毫秒为单位。
    system.profile.planSummary:新版本3.4。执行计划的摘要。
    system.profile.execStats:在3.0版本中进行了更改。包含查询操作执行统计信息的文档。对于其他操作,该值是一个空文档。
    以树的形式显示统计数据;每个节点提供查询操作的该阶段中执行的操作的统计信息。

    system.profile.execStats.stage:新版本3.0:stage替换type字段。查询执行过程中执行的操作的描述性名称;如。

    • COLLSCAN 全表扫描
    • IXSCAN 使用索引
    • FETCH 根据索引去检索文档

    system.profile.execStats.inputStages:新版本3.0:inputstage替换了子字段。包含当前阶段的输入阶段的操作的统计信息的数组。

    system.profile.ts:操作的时间戳。
    system.profile.client:发起操作的客户机连接的IP地址或主机名。
    对于某些操作,例如db.eval(),客户机是0.0.0.0:0,而不是实际的客户机。
    system.profile.appName:新版本3.4。运行该操作的客户机应用程序的标识符。使用appName连接字符串选项为appName字段设置自定义值。
    system.profile.allUsers:会话的经过身份验证的用户信息(用户名和数据库)数组。参见用户。
    system.profile.user:运行操作的经过身份验证的用户。如果操作不是由经过身份验证的用户运行的,则该字段的值为空字符串。

    1:参数含义

    复制代码
    drug:PRIMARY> db.system.profile.find().pretty()
    {
        "op" : "query",    #操作类型,有insert、query、update、remove、getmore、command   
        "ns" : "mc.user",  #操作的集合
        "query" : {        #查询语句
            "mp_id" : 5,
            "is_fans" : 1,
            "latestTime" : {
                "$ne" : 0
            },
            "latestMsgId" : {
                "$gt" : 0
            },
            "$where" : "new Date(this.latestNormalTime)>new Date(this.replyTime)"
        },
        "cursorid" : NumberLong("1475423943124458998"),
        "ntoreturn" : 0,   #返回的记录数。例如,profile命令将返回一个文档(一个结果文件),因此ntoreturn值将为1。limit(5)命令将返回五个文件,因此ntoreturn值是5。如果ntoreturn值为0,则该命令没有指定一些文件返回,因为会是这样一个简单的find()命令没有指定的限制。
        "ntoskip" : 0,     #skip()方法指定的跳跃数
        "nscanned" : 304,  #扫描数量
        "keyUpdates" : 0,  #索引更新的数量,改变一个索引键带有一个小的性能开销,因为数据库必须删除旧的key,并插入一个新的key到B-树索引
        "numYield" : 0,    #该查询为其他查询让出锁的次数
        "lockStats" : {    #锁信息,R:全局读锁;W:全局写锁;r:特定数据库的读锁;w:特定数据库的写锁
            "timeLockedMicros" : {     #锁
                "r" : NumberLong(19467),
                "w" : NumberLong(0)
            },
            "timeAcquiringMicros" : {  #锁等待
                "r" : NumberLong(7),
                "w" : NumberLong(9)
            }
        },
        "nreturned" : 101,        #返回的数量
        "responseLength" : 74659, #响应字节长度
        "millis" : 19,            #消耗的时间(毫秒)
        "ts" : ISODate("2014-02-25T02:13:54.899Z"), #语句执行的时间
        "client" : "127.0.0.1",   #链接ip或则主机
        "allUsers" : [ ],     
        "user" : ""               #用户
    }
    复制代码
    除上面外还有:
    复制代码
    scanAndOrder:
    scanAndOrder是一个布尔值,是True当一个查询不能使用的文件的顺序在索引中的排序返回结果:MongoDB中必须将其接收到的文件从一个游标后的文件进行排序。
    如果scanAndOrder是False,MongoDB的可使用这些文件的顺序索引返回排序的结果。即:True:文档进行排序,False:使用索引。
    
    moved
    更新操作在磁盘上移动一个或多个文件到新的位置。表明本次update是否移动了硬盘上的数据,如果新记录比原记录短,通常不会移动当前记录,如果新记录比原记录长,那么可能会移动记录到其它位置,这时候会导致相关索引的更新.磁盘操作更多,加上索引
    更新,会使得这样的操作比较慢.
    nmoved:
    文件在磁盘上操作。
    
    nupdated:
    更新文档的数目
    复制代码

    getmore是一个getmore 操作,getmore通常发生在结果集比较大的查询时,第一个query返回了部分结果,后续的结果是通过getmore来获取的。

    如果nscanned(扫描的记录数)远大于nreturned(返回结果的记录数)的话,要考虑通过加索引来优化记录定位了。responseLength 如果过大,说明返回的结果集太大了,这时要看是否只需要必要的字段。

    2:日常使用的查询

    复制代码
    #返回最近的10条记录
    db.system.profile.find().limit(10).sort({ ts : -1 }).pretty()
    
    #返回所有的操作,除command类型的
    db.system.profile.find( { op: { $ne : 'command' } } ).pretty()
    
    #返回特定集合
    db.system.profile.find( { ns : 'mydb.test' } ).pretty()
    
    #返回大于5毫秒慢的操作
    db.system.profile.find( { millis : { $gt : 5 } } ).pretty()
    
    #从一个特定的时间范围内返回信息
    db.system.profile.find(
                           {
                            ts : {
                                  $gt : new ISODate("2012-12-09T03:00:00Z") ,
                                  $lt : new ISODate("2012-12-09T03:40:00Z")
                                 }
                           }
                          ).pretty()
    
    #特定时间,限制用户,按照消耗时间排序
    db.system.profile.find(
                           {
                             ts : {
                                   $gt : new ISODate("2011-07-12T03:00:00Z") ,
                                   $lt : new ISODate("2011-07-12T03:40:00Z")
                                  }
                           },
                           { user : 0 }
                          ).sort( { millis : -1 } )
    复制代码
     
    > db.system.profile.find() 
      {"ts" : "Thu Jan 29 2009 15:19:32 GMT-0500 (EST)" , "info" : "query test.$cmd ntoreturn:1 reslen:66 nscanned:0 
      query: { profile: 2 } nreturned:1 bytes:50" , "millis" : 0} 
      db.system.profile.find( { info: /test.foo/ } ) 
      {"ts" : "Thu Jan 29 2009 15:19:40 GMT-0500 (EST)" , "info" : "insert test.foo" , "millis" : 0} 
      {"ts" : "Thu Jan 29 2009 15:19:42 GMT-0500 (EST)" , "info" : "insert test.foo" , "millis" : 0} 
      {"ts" : "Thu Jan 29 2009 15:19:45 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 reslen:102 nscanned:2 
      query: {} nreturned:2 bytes:86" , "millis" : 0} 
      {"ts" : "Thu Jan 29 2009 15:21:17 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 reslen:36 nscanned:2 
      query: { $not: { x: 2 } } nreturned:0 bytes:20" , "millis" : 0} 
      {"ts" : "Thu Jan 29 2009 15:21:27 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 exception bytes:53" , "millis" : 88}

    列出执行时间长于某一限度(5ms)的 Profile 记录:

    > db.system.profile.find( { millis : { $gt : 5 } } ) 
      {"ts" : "Thu Jan 29 2009 15:21:27 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 exception bytes:53" , "millis" : 88}

    查看最新的 Profile 记录:

    db.system.profile.find().sort({$natural:-1})

    Mongo Shell 还提供了一个比较简洁的命令show profile,可列出最近5条执行时间超过1ms的 Profile 记录。

    Profile 信息内容详解:

      ts-该命令在何时执行.

      millis Time-该命令执行耗时,以毫秒记.

      info-本命令的详细信息.

      query-表明这是一个query查询操作.

      ntoreturn-本次查询客户端要求返回的记录数.比如, findOne()命令执行时 ntoreturn 为 1.有limit(n) 条件时ntoreturn为n.

      query-具体的查询条件(如x>3).

      nscanned-本次查询扫描的记录数.

      reslen-返回结果集的大小.

      nreturned-本次查询实际返回的结果集.

      update-表明这是一个update更新操作.

      fastmod-Indicates a fast modify operation. See Updates. These operations are normally quite fast.

      fastmodinsert – indicates a fast modify operation that performed an upsert.

      upsert-表明update的upsert参数为true.此参数的功能是如果update的记录不存在,则用update的条件insert一条记录.

      moved-表明本次update是否移动了硬盘上的数据,如果新记录比原记录短,通常不会移动当前记录,如果新记录比原记录长,那么可能会移动记录到其它位置,这时候会导致相关索引的更新.磁盘操作更多,加上索引更新,会使得这样的操作比较慢.

      insert-这是一个insert插入操作.

      getmore-这是一个getmore 操作,getmore通常发生在结果集比较大的查询时,第一个query返回了部分结果,后续的结果是通过getmore来获取的。

    Profiler 的效率

    Profiling 功能肯定是会影响效率的,但是不太严重,原因是他使用的是system.profile 来记录,而system.profile 是一个capped collection 这种collection 在操作上有一些限制和特点,但是效率更高。

    示例:

    复制代码
    #返回最近的10条记录
    db.system.profile.find().limit(10).sort({ ts : -1 }).pretty()
    
    #返回所有的操作,除command类型的
    db.system.profile.find( { op: { $ne : 'command' } } ).pretty()
    
    #返回特定集合
    db.system.profile.find( { ns : 'mydb.test' } ).pretty()
    
    #返回大于5毫秒慢的操作
    db.system.profile.find( { millis : { $gt : 5 } } ).pretty()
    
    #从一个特定的时间范围内返回信息
    db.system.profile.find(
                           {
                            ts : {
                                  $gt : new ISODate("2012-12-09T03:00:00Z") ,
                                  $lt : new ISODate("2012-12-09T03:40:00Z")
                                 }
                           }
                          ).pretty()
    
    #特定时间,限制用户,按照消耗时间排序
    db.system.profile.find(
                           {
                             ts : {
                                   $gt : new ISODate("2011-07-12T03:00:00Z") ,
                                   $lt : new ISODate("2011-07-12T03:40:00Z")
                                  }
                           },
                           { user : 0 }
                          ).sort( { millis : -1 } )
    复制代码

    mongodb启用Profiling定位问题

    建议使用方法三,最简单且容易查看。

    方法一:记录所有日志,查看mongod.log

    如果mongodb已经运行了很长时间,此时查看mongod.log很大,没法打开查看相应信息

    #ps -ef|grep mongod

    找到相应的mongod的进程的pid

    然后kill -SIGUSR1 pid  这样mongod就会重新生成一个日志文件

    > db.setProfilingLevel(2); 
      {"was" : 0 , "ok" : 1} 
      > db.getProfilingLevel()
      上面斜体的级别可以取0,1,2 三个值,他们表示的意义如下:
      0 – 不开启

      1 – 记录慢命令 (默认为>100ms)

      2 – 记录所有命令

    开启profiling后在mongod节点的log日志中查看

    cat mongod.log

    或是tail -300 mongod.log

    二.

    以后你们遇到了这种某个操作慢的问题直接从数据库里在小数据下打开第5级log,看看它访问了那些表格,直接去看Py中有没有index
    一般说来只要系统不瘫痪,没有index是慢的直接原因
    db.adminCommand({logRotage:1})
    kill -SIGUSR1 xxx
    db.setLogLevel(1, "query")
    db.setLogLevel(1, "command")

    为了以后尽快的定位专属MongoDB的问题,请你们如果发现哪个操作或者并行操作性能慢的话帮忙抓取一下MongoDB log。

    分为两方面:

    Profiling: 如果我们定义所有的慢于100ms的数据库操作多为慢操作的话做如下操作:

    复制代码
    use MyDomain
    db.setProfilingLevel(1, 100), 用你测试的Domain 替换MyDomain
    这样所有的慢操作都会在 db.system.profile这里显示。
    
    db.setProfilingLevel(2) 是打开所有操作profile, 可以实验用,或者短时打开。但是一定要及时关闭。
    
    show collections 会看到多了一个collection  叫system.profile
    
    然后db.system.profile.find().pretty()
    复制代码

    Log: 用db.setLogLevel(1,command)可以看到所有的操作,每个操作的用时都会记录下来。

    记得定位后用db.setLogLevel(0,command)改回去。

  • 相关阅读:
    android 入门-Service
    android 入门-Activity及 字体
    android 入门-安装环境
    PS 零基础训练1
    wp8 入门到精通 ImageCompress 图片压缩
    wp8 入门到精通 Gallery
    C# DateTime时间格式转换为Unix时间戳格式
    wp8 入门到精通 MultiMsgPrompt
    C# Func<T,TResult>
    redis简介
  • 原文地址:https://www.cnblogs.com/duanxz/p/3746122.html
Copyright © 2011-2022 走看看