引入
在业务开发中,我们常把数据持久化到数据库中。如果需要读取这些数据,除了直接从数据库中读取外,为了减轻数据库的访问压力以及提高访问速度,我们更多地引入缓存来对数据进行存取。读取数据的过程一般为:

图1:加入缓存的数据读取过程
对于分布式缓存,不同机器上存储不同对象的数据。为了实现这些缓存机器的负载均衡,可以使用式子1来定位对象缓存的存储机器:

m = hash(o) mod n ——式子1

其中,o为对象的名称,n为机器的数量,m为机器的编号,hash为一hash函数。图2中的负载均衡器(load balancer)正是使用式子1来将客户端对不同对象的请求分派到不同的机器上执行,例如,对于对象o,经过式子1的计算,得到m的值为3,那么所有对对象o的读取和存储的请求都被发往机器3执行。

图2:如何利用Hash取模实现负载均衡
式子1在大部分时候都可以工作得很好,然而,当机器需要扩容或者机器出现宕机的情况下,事情就比较棘手了。
当机器扩容,需要增加一台缓存机器时,负载均衡器使用的式子变成:

m = hash(o) mod (n + 1) ——式子2

当机器宕机,机器数量减少一台时,负载均衡器使用的式子变成:

m = hash(o) mod (n - 1) ——式子3
我们以机器扩容的情况为例,说明简单的取模方法会导致什么问题。假设机器由3台变成4台,对象o1由式子1计算得到的m值为2,由式子2计算得到的m值却可能为0,1,2,3(一个 3t + 2的整数对4取模,其值可能为0,1,2,3,读者可以自行验证),大约有75%(3/4)的可能性出现缓存访问不命中的现象。随着机器集群规模的扩大,这个比例线性上升。当99台机器再加入1台机器时,不命中的概率是99%(99/100)。这样的结果显然是不能接受的,因为这会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

一致性hash算法正是为了解决此类问题的方法,它可以保证当机器增加或者减少时,对缓存访问命中的概率影响减至很小。下面我们来详细说一下一致性hash算法的具体过程。
一致性Hash环
一致性hash算法通过一个叫作一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1],如下图3所示:

图3:一致性Hash环
将对象放置到Hash环
假设现在我们有4个对象,分别为o1,o2,o3,o4,使用hash函数计算这4个对象的hash值(范围为0 ~ 2^32-1):

hash(o1) = m1
hash(o2) = m2
hash(o3) = m3
hash(o4) = m4

把m1,m2,m3,m4这4个值放置到hash环上,得到如下图4:

图4:放置了对象的一致性Hash环

将机器放置到Hash环
使用同样的hash函数,我们将机器也放置到hash环上。假设我们有三台缓存机器,分别为 c1,c2,c3,使用hash函数计算这3台机器的hash值:

hash(c1) = t1
hash(c2) = t2
hash(c3) = t3

把t1,t2,t3 这3个值放置到hash环上,得到如下图5:

图5:放置了机器的一致性Hash环
为对象选择机器
将对象和机器都放置到同一个hash环后,在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。
例如,对于对象o2,顺序针找到最近的机器是c1,故机器c1会缓存对象o2。而机器c2则缓存o3,o4,机器c3则缓存对象o1。

图6:在一致性Hash环上为对象选择机器

处理机器增减的情况
对于线上的业务,增加或者减少一台机器的部署是常有的事情。
例如,增加机器c4的部署并将机器c4加入到hash环的机器c3与c2之间。这时,只有机器c3与c4之间的对象需要重新分配新的机器。对于我们的例子,只有对象o4被重新分配到了c4,其他对象仍在原有机器上。如图7所示:

图7:增加机器后的一致性Hash环的结构

如上文前面所述,使用简单的求模方法,当新添加机器后会导致大部分缓存失效的情况,使用一致性hash算法后这种情况则会得到大大的改善。前面提到3台机器变成4台机器后,缓存命中率只有25%(不命中率75%)。而使用一致性hash算法,理想情况下缓存命中率则有75%,而且,随着机器规模的增加,命中率会进一步提高,99台机器增加一台后,命中率达到99%,这大大减轻了增加缓存机器带来的数据库访问的压力。

再例如,将机器c1下线(当然,也有可能是机器c1宕机),这时,只有原有被分配到机器c1对象需要被重新分配到新的机器。对于我们的例子,只有对象o2被重新分配到机器c3,其他对象仍在原有机器上。如图8所示:

图8:减少机器后的一致性Hash环的结构

虚拟节点
上面提到的过程基本上就是一致性hash的基本原理了,不过还有一个小小的问题。新加入的机器c4只分担了机器c2的负载,机器c1与c3的负载并没有因为机器c4的加入而减少负载压力。如果4台机器的性能是一样的,那么这种结果并不是我们想要的。
为此,我们引入虚拟节点来解决负载不均衡的问题。
将每台物理机器虚拟为一组虚拟机器,将虚拟机器放置到hash环上,如果需要确定对象的机器,先确定对象的虚拟机器,再由虚拟机器确定物理机器。
说得有点复杂,其实过程也很简单。

还是使用上面的例子,假如开始时存在缓存机器c1,c2,c3,对于每个缓存机器,都有3个虚拟节点对应,其一致性hash环结构如图9所示:

图9:机器c1,c2,c3的一致性Hash环结构

假设对于对象o1,其对应的虚拟节点为c11,而虚拟节点c11对象缓存机器c1,故对象o1被分配到机器c1中。

新加入缓存机器c4,其对应的虚拟节点为c41,c42,c43,将这三个虚拟节点添加到hash环中,得到的hash环结构如图10所示:

图10:机器c1,c2,c3,c4的一致性Hash环结构

新加入的缓存机器c4对应一组虚拟节点c41,c42,c43,加入到hash环后,影响的虚拟节点包括c31,c22,c11(顺时针查找到第一个节点),而这3个虚拟节点分别对应机器c3,c2,c1。即新加入的一台机器,同时影响到原有的3台机器。理想情况下,新加入的机器平等地分担了原有机器的负载,这正是虚拟节点带来的好处。而且新加入机器c4后,只影响25%(1/4)对象分配,也就是说,命中率仍然有75%,这跟没有使用虚拟节点的一致性hash算法得到的结果是相同的。

总结
一致性hash算法解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。通过虚拟节点的使用,一致性hash算法可以均匀分担机器的负载,使得这一算法更具现实的意义。正因如此,一致性hash算法被广泛应用于分布式系统中。

参考资料
https://en.wikipedia.org/wiki/Consistent_hashing

https://www.codeproject.com/articles/56138/consistent-hashing

《大型网站技术架构——核心原理与安全分析》,李智慧著,电子工业出版社


作者:haozlee
来源:CSDN
原文:https://blog.csdn.net/lihao21/article/details/54193868
版权声明:本文为博主原创文章,转载请附上博文链接!
一致性hash算法的PHP实现

<?php
/**

  • Flexihash - A simple consistent hashing implementation for PHP.
  • The MIT License
  • Copyright (c) 2008 Paul Annesley
  • Permission is hereby granted, free of charge, to any person obtaining a copy
  • of this software and associated documentation files (the "Software"), to deal
  • in the Software without restriction, including without limitation the rights
  • to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  • copies of the Software, and to permit persons to whom the Software is
  • furnished to do so, subject to the following conditions:
  • The above copyright notice and this permission notice shall be included in
  • all copies or substantial portions of the Software.
  • THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  • IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  • FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  • AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  • LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  • OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  • THE SOFTWARE.
  • @author Paul Annesley
  • @link http://paul.annesley.cc/
  • @copyright Paul Annesley, 2008
  • @comment by MyZ (http://blog.csdn.net/mayongzhan)
    */

/**

  • A simple consistent hashing implementation with pluggable hash algorithms.
  • @author Paul Annesley
  • @package Flexihash
  • @licence http://www.opensource.org/licenses/mit-license.php
    */
    class Flexihash
    {
    /**

    • The number of positions to hash each target to.
    • @var int
    • @comment 虚拟节点数,解决节点分布不均的问题
      */
      private $_replicas = 64;

    /**

    • The hash algorithm, encapsulated in a Flexihash_Hasher implementation.
    • @var object Flexihash_Hasher
    • @comment 使用的hash方法 : md5,crc32
      */
      private $_hasher;

    /**

    • Internal counter for current number of targets.
    • @var int
    • @comment 节点记数器
      */
      private $_targetCount = 0;

    /**

    • Internal map of positions (hash outputs) to targets
    • @var array { position => target, ... }
    • @comment 位置对应节点,用于lookup中根据位置确定要访问的节点
      */
      private $_positionToTarget = array();

    /**

    • Internal map of targets to lists of positions that target is hashed to.
    • @var array { target => [ position, position, ... ], ... }
    • @comment 节点对应位置,用于删除节点
      */
      private $_targetToPositions = array();

    /**

    • Whether the internal map of positions to targets is already sorted.
    • @var boolean
    • @comment 是否已排序
      */
      private $_positionToTargetSorted = false;

    /**

    • Constructor
    • @param object $hasher Flexihash_Hasher
    • @param int $replicas Amount of positions to hash each target to.
    • @comment 构造函数,确定要使用的hash方法和需拟节点数,虚拟节点数越多,分布越均匀,但程序的分布式运算越慢
      */
      public function __construct(Flexihash_Hasher $hasher = null, $replicas = null)
      {
      $this->_hasher = $hasher ? $hasher : new Flexihash_Crc32Hasher();
      if (!empty($replicas)) $this->_replicas = $replicas;
      }

    /**

    • Add a target.
    • @param string $target
    • @chainable
    • @comment 添加节点,根据虚拟节点数,将节点分布到多个虚拟位置上
      */
      public function addTarget($target)
      {
      if (isset($this->_targetToPositions[$target]))
      {
      throw new Flexihash_Exception("Target '$target' already exists.");
      }

      $this->_targetToPositions[$target] = array();

      // hash the target into multiple positions
      for ($i = 0; $i < $this->_replicas; $i++)
      {
      $position = $this->_hasher->hash($target . $i);
      $this->_positionToTarget[$position] = $target; // lookup
      $this->_targetToPositions[$target] []= $position; // target removal
      }

      $this->_positionToTargetSorted = false;
      $this->_targetCount++;

      return $this;
      }

    /**

    • Add a list of targets.
    • @param array $targets
    • @chainable
      */
      public function addTargets($targets)
      {
      foreach ($targets as $target)
      {
      $this->addTarget($target);
      }

      return $this;
      }

    /**

    • Remove a target.
    • @param string $target
    • @chainable
      */
      public function removeTarget($target)
      {
      if (!isset($this->_targetToPositions[$target]))
      {
      throw new Flexihash_Exception("Target '$target' does not exist.");
      }

      foreach ($this->_targetToPositions[$target] as $position)
      {
      unset($this->_positionToTarget[$position]);
      }

      unset($this->_targetToPositions[$target]);

      $this->_targetCount--;

      return $this;
      }

    /**

    • A list of all potential targets
    • @return array
      */
      public function getAllTargets()
      {
      return array_keys($this->_targetToPositions);
      }

    /**

    • A list of all potential targets
    • @return array
      */
      public function getAll()
      {
      return array(
      "targers"=>$this->_positionToTarget,
      "positions"=>$this->_targetToPositions);
      }

    /**

    • Looks up the target for the given resource.
    • @param string $resource
    • @return string
      */
      public function lookup($resource)
      {
      $targets = $this->lookupList($resource, 1);
      if (empty($targets)) throw new Flexihash_Exception('No targets exist');
      return $targets[0]; //0表示返回离资源位置最近的机器节点
      }

    /**

    • Get a list of targets for the resource, in order of precedence.
    • Up to $requestedCount targets are returned, less if there are fewer in total.
    • @param string $resource
    • @param int $requestedCount The length of the list to return
    • @return array List of targets
    • @comment 查找当前的资源对应的节点,
    • 节点为空则返回空,节点只有一个则返回该节点,
    • 对当前资源进行hash,对所有的位置进行排序,在有序的位置列上寻找当前资源的位置
    • 当全部没有找到的时候,将资源的位置确定为有序位置的第一个(形成一个环)
    • 返回所找到的节点
      */
      public function lookupList($resource, $requestedCount)
      {
      if (!$requestedCount)
      throw new Flexihash_Exception('Invalid count requested');

      // handle no targets
      if (empty($this->_positionToTarget))
      return array();

      // optimize single target
      if ($this->_targetCount == 1)
      return array_unique(array_values($this->_positionToTarget));

      // hash resource to a position
      $resourcePosition = $this->_hasher->hash($resource);

      $results = array();
      $collect = false;

      $this->_sortPositionTargets();

      // search values above the resourcePosition
      foreach ($this->_positionToTarget as $key => $value)
      {
      // start collecting targets after passing resource position
      if (!$collect && $key > $resourcePosition)
      {
      $collect = true;
      }

      // only collect the first instance of any target
      if ($collect && !in_array($value, $results))
      {$results []= $value;//var_dump($results);
      }
      // return when enough results, or list exhausted
      //var_dump(count($results));
      //var_dump($requestedCount);
      if (count($results) == $requestedCount || count($results) == $this->_targetCount)
      {return $results;
      }

      }

      // loop to start - search values below the resourcePosition
      foreach ($this->_positionToTarget as $key => $value)
      {
      if (!in_array($value, $results))
      {
      $results []= $value;
      }

      // return when enough results, or list exhausted
      if (count($results) == $requestedCount || count($results) == $this->_targetCount)
      {return $results;
      }

      }

      // return results after iterating through both "parts"
      return $results;
      }

    public function __toString()
    {
    return sprintf(
    '%s{targets:[%s]}',
    get_class($this),
    implode(',', $this->getAllTargets())
    );
    }

    // ----------------------------------------
    // private methods

    /**

    • Sorts the internal mapping (positions to targets) by position
      */
      private function _sortPositionTargets()
      {
      // sort by key (position) if not already
      if (!$this->_positionToTargetSorted)
      {
      ksort($this->_positionToTarget, SORT_REGULAR);
      $this->_positionToTargetSorted = true;
      }
      }

}

/**

  • Hashes given values into a sortable fixed size address space.
  • @author Paul Annesley
  • @package Flexihash
  • @licence http://www.opensource.org/licenses/mit-license.php
    */
    interface Flexihash_Hasher
    {

    /**

    • Hashes the given string into a 32bit address space.
    • Note that the output may be more than 32bits of raw data, for example
    • hexidecimal characters representing a 32bit value.
    • The data must have 0xFFFFFFFF possible values, and be sortable by
    • PHP sort functions using SORT_REGULAR.
    • @param string
    • @return mixed A sortable format with 0xFFFFFFFF possible values
      */
      public function hash($string);

}

/**

  • Uses CRC32 to hash a value into a signed 32bit int address space.
  • Under 32bit PHP this (safely) overflows into negatives ints.
  • @author Paul Annesley
  • @package Flexihash
  • @licence http://www.opensource.org/licenses/mit-license.php
    */
    class Flexihash_Crc32Hasher
    implements Flexihash_Hasher
    {

    /* (non-phpdoc)

    • @see Flexihash_Hasher::hash()
      */
      public function hash($string)
      {
      return crc32($string);
      }

}

/**

  • Uses CRC32 to hash a value into a 32bit binary string data address space.
  • @author Paul Annesley
  • @package Flexihash
  • @licence http://www.opensource.org/licenses/mit-license.php
    */
    class Flexihash_Md5Hasher
    implements Flexihash_Hasher
    {

    /* (non-phpdoc)

    • @see Flexihash_Hasher::hash()
      */
      public function hash($string)
      {
      return substr(md5($string), 0, 8); // 8 hexits = 32bit

      // 4 bytes of binary md5 data could also be used, but
      // performance seems to be the same.
      }

}

/**

  • An exception thrown by Flexihash.
  • @author Paul Annesley
  • @package Flexihash
  • @licence http://www.opensource.org/licenses/mit-license.php
    */
    class Flexihash_Exception extends Exception
    {
    }
    复制代码
    测试代码

复制代码
$hash = new Flexihash();
$targets=array(
"192.168.1.1:11011",
"192.168.1.1:11012",
"192.168.1.1:11013",
"192.168.1.1:11014",
"192.168.1.1:11015",
);
$hash->addTargets($targets);
for ($i=0; $i < 25; $i++) {
$resource = sprintf("format %d",$i);
var_dump($resource." --> ".$hash->lookup($resource));
}
输出

复制代码
string(30) "format 0 --> 192.168.1.1:11015"
string(30) "format 1 --> 192.168.1.1:11015"
string(30) "format 2 --> 192.168.1.1:11015"
string(30) "format 3 --> 192.168.1.1:11015"
string(30) "format 4 --> 192.168.1.1:11011"
string(30) "format 5 --> 192.168.1.1:11011"
string(30) "format 6 --> 192.168.1.1:11011"
string(30) "format 7 --> 192.168.1.1:11011"
string(30) "format 8 --> 192.168.1.1:11012"
string(30) "format 9 --> 192.168.1.1:11013"
string(31) "format 10 --> 192.168.1.1:11013"
string(31) "format 11 --> 192.168.1.1:11011"
string(31) "format 12 --> 192.168.1.1:11012"
string(31) "format 13 --> 192.168.1.1:11011"
string(31) "format 14 --> 192.168.1.1:11014"
string(31) "format 15 --> 192.168.1.1:11014"
string(31) "format 16 --> 192.168.1.1:11014"
string(31) "format 17 --> 192.168.1.1:11014"
string(31) "format 18 --> 192.168.1.1:11012"
string(31) "format 19 --> 192.168.1.1:11012"
string(31) "format 20 --> 192.168.1.1:11013"
string(31) "format 21 --> 192.168.1.1:11012"
string(31) "format 22 --> 192.168.1.1:11012"
string(31) "format 23 --> 192.168.1.1:11014"
string(31) "format 24 --> 192.168.1.1:11012"
[Finished in 0.1s]
复制代码
redis分布式代码设计

<?php
require_once("Flexihash.php");
$config=array(
"127.0.0.1:6371",
"127.0.0.1:6372",
"127.0.0.1:6373",
"127.0.0.1:6374",
);
class RedisCollect {
//redis实例
private $_redis = null;
//hash实例
private $_hash = null;
//初始化
public function __construct() {
global $config;
$this->_redis = new Redis();
$this->_hash = new Flexihash();
$this->_hash->addTargets($config);
}
public function set($key="", $value="") {
$m = $this->switchConncetion($key);
return $m->set($key, $value);
}
public function get($key) {
$m = $this->switchConncetion($key);
return $m->get($key);
}
private function switchConncetion($key) {
$hostinfo = $this->_hash->lookup($key);
$m = $this->connect($hostinfo);
return $m;
}
private function connect($hostinfo) {
list($host, $port) = explode(":", $hostinfo);
//printf("host = %s, port = %s\n",$host,$port);
if(empty($host) || empty($port)) {
return false;
}
try {
$this->_redis->connect($host, $port);
return $this->_redis;
} catch(Exception $e) {
die($e->getMessage());
}
}
}

转载于:https://blog.51cto.com/14219632/2356363

一致性hash原理及php实例相关推荐

  1. hash地址_深入浅出一致性Hash原理

    一.前言 在解决分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡的作用. 但是普通的余数h ...

  2. 浅谈一致性Hash原理及应用

    在讲一致性Hash之前我们先来讨论一个问题. 问题:现在有亿级用户,每日产生千万级订单,如何将订单进行分片分表? 小A:我们可以按照手机号的尾数进行分片,同一个尾数的手机号写入同一片/同一表中. 大佬 ...

  3. 求锤得锤,你要的一致性 hash 来了! | 附代码

    来源 | BigData之路 头图 | 视觉中国 前言 最近总有人问我一致性hash的事情,求锤得锤,我们今天就来聊聊看.前两篇我们分别介绍了两类哈希分片的方法:hash取模和虚拟桶. hash取模法 ...

  4. 分布式应用解决方案之一致性Hash

    什么是一致性Hash 一致性Hash就是将整个hash值空间按照顺时针方向形成一个虚拟的环,整个环状结构就称之为Hash环.那为什么叫做一致性Hash环?一致性是由于Hash环应用场景一般在分布式应用 ...

  5. java hash取模,一致性hash算法及其java实现

    目录 背景 随着业务系统越来越大,我们需要对API的访问进行更多的缓存,使用Redis是一个很好的解决方案. 但是单台Redis性能不足够且迟早要走向集群的,那么怎么才能良好的利用Redis集群来进行 ...

  6. 一致性hash算法-hash环-数据倾斜

    一.前言 在解决分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡的作用. 但是普通的余数h ...

  7. Maglev 一致性Hash调研

    dpvs中conhash实现 原理 ● 一个RS对应的虚拟节点(副本)的个数  replicas = Weight * REPLICA ● 虚拟节点的 hash_key     hash_key= r ...

  8. 给面试官讲明白:一致性Hash的原理和实践

    戳蓝字"CSDN云计算"关注我们哦! 来源 | 靳刚同学 作者 | 靳刚 "一致性hash的设计初衷是解决分布式缓存问题,它不仅能起到hash作用,还可以在服务器宕机时, ...

  9. Mycat(三)——几种分片规则 分库 一致性hash的原理及使用

    文章目录 Mycat分片规则 取模 分库 schema.xml 分片枚举 schema.xml 测试 问题: 固定hash分片 优点 范围约定分片 按日期分区 按天分 一致性HASH 解决什么问题? ...

  10. Java算法之 一致性hash算法原理及实现

    为什么80%的码农都做不了架构师?>>>    一致性hash算法原理及实现 转载于:https://my.oschina.net/90888/blog/1645131

最新文章

  1. 破解网页禁止使用“复制”、“粘贴“
  2. DPDK架构与特点(转)
  3. shell实例第17讲:连续输入4个100以内的数字,统计和、乘、平均、最小和最大
  4. 编写程序输入一个5x5的矩阵,将最大元素与中心元素交换,并按行列对齐输出。...
  5. ABP vNext微服务架构详细教程——简介
  6. jmeter聚个报告怎么看qps_Jmeter 使用笔记之 html 报告扩展(一)
  7. 华为弹性云服务器ECS使用学习0
  8. java无限循环可变参数,Java可变参数、加强for循环
  9. pythonweb自动化项目源码下载_python+requests接口自动化完整项目设计源码
  10. yolov4实现口罩佩戴检测,在验证集上做到了0.954的mAP
  11. java格斗代码_java学习2打架代码编写
  12. ASM1117-3.3V稳压芯片的典型电路图及分析
  13. 条码扫描枪在仓库管理无线网络AP解决方案
  14. python运行系统找不到指定文件_系统找不到指定文件怎么解决
  15. 愿以三生烟火,换君一世迷离
  16. 中小银行数字普惠金融发展的思考和建议
  17. [AHK]按文件的年份归档到不同年份目录里
  18. 微软2016校园招聘4月在线笔试2-403 Forbidden
  19. bitlocker加密怎么解除(破解Bitlocker加密方法)
  20. Webpack项目中引入Bootstrap4.x

热门文章

  1. 日均百亿级日志处理:微博基于 Flink 的实时计算平台建设
  2. 动图:七分钟帮你理解什么是KMP算法?
  3. 送书,手快有!!Android音视频开发、Android进阶解密
  4. python图形库使用教程_PySide——Python图形化界面入门教程(四)
  5. 十六进制数相加校验和计算程序
  6. VMware三种工作模式
  7. k3cloud新建简单帐表教程
  8. Puppet常用资源使用详解
  9. express+mongodb制作简单的文章发布系统(一)之环境的搭建
  10. Android零基础入门第66节:RecyclerView点击事件处理