【问题标题】:Image upload storage strategies图片上传存储策略
【发布时间】:2011-02-08 13:43:14
【问题描述】:

当用户上传图片到我的网站时,图片会经过这个过程;

  • 用户上传图片
  • 将图片元数据存储在数据库中,为图片赋予唯一的ID
  • 异步图像处理(缩略图创建、裁剪等)
  • 所有图片都存储在同一个上传文件夹中

到目前为止,该网站还很小,上传目录中只有大约 200,000 张图片。我意识到我远未达到目录中文件的物理限制,但这种方法显然无法扩展,所以我想知道是否有人对处理大量图像上传的上传/存储策略有任何建议。

编辑: 创建用户名(或更具体地说,用户 ID)子文件夹似乎是一个很好的解决方案。通过更多的挖掘,我在这里找到了一些很棒的信息; How to store images in your filesystem
但是,如果购买 CDN,这种 userid dir 方法是否可以很好地扩展?

【问题讨论】:

  • 您是否考虑过为每个用户创建一个文件夹,可能使用 /letter/username 格式(例如 images/o/omg_unicornsimages/p/powerlord
  • 可行,但用户名可以更改。我将编辑并添加此信息。

标签: php file upload storage


【解决方案1】:

MediaWiki 生成上传文件名称的 MD5 和,并使用 MD5 的前两个字母(例如,和“cf1e66b779​​18167a6b6b972c12b1c00d”的“c”和“f”)创建此目录结构:

images/c/cf/Whatever_filename.png

您还可以使用图像 ID 来确定每个目录的文件数上限。也许用floor(image unique ID / 1000) 来确定父目录,每个目录有 1000 张图像。

【讨论】:

  • 我们使用类似的方法,但采用 4 级深度结构:12/34/56/78 适用于数百万个文件。
  • 图片ID是什么?如何在 PHP 中找到这个?
  • +用户随意为自行车涂上你喜欢的任何颜色。
【解决方案2】:

您可以考虑使用开源 http://danga.com/mogilefs/,因为它非常适合您的工作。它将带您从考虑文件夹到名称空间(可能是用户)并让它为您存储图像。最好的部分是您不必关心数据的存储方式。它使其完全冗余,您甚至可以设置关于冗余缩略图的控制。

【讨论】:

    【解决方案3】:

    您是否考虑过使用 Amazon S3 之类的东西来存储文件?我经营一家照片托管公司,在我们自己的服务器上很快达到限制后,我们切换到了 AmazonS3。 S3 的美妙之处在于没有 inode 之类的限制,您只需不断向其扔文件即可。

    另外:如果您不喜欢 S3,您可以随时尝试将其分解为尽可能多的子文​​件夹:

    /userid/年/月/日/photoid.jpg

    【讨论】:

      【解决方案4】:

      我之前回答过一个类似的问题,但我找不到它,也许 OP 删除了他的问题......

      无论如何,Adams solution 似乎是迄今为止最好的,但它不是防弹的,因为images/c/cf/(或任何其他目录/子目录对)仍可包含多达 16^30 个唯一哈希 如果我们计算图像扩展名,文件数量至少要多 3 倍,远远超过任何常规文件系统可以处理的数量。

      AFAIK,SourceForge.net 也将此系统用于项目存储库,例如 "fatfree" project 将放置在 projects/f/fa/fatfree/,但我相信他们将项目名称限制为 8 个字符。


      我会在数据库中存储图像哈希以及 DATE / DATETIME / TIMESTAMP 字段,指示图像何时上传/处理,然后将图像放置在如下结构中:

      images/
        2010/                                      - Year
          04/                                      - Month
            19/                                    - Day
              231c2ee287d639adda1cdb44c189ae93.png - Image Hash
      

      或者:

      images/
        2010/                                    - Year
          0419/                                  - Month & Day (12 * 31 = 372)
            231c2ee287d639adda1cdb44c189ae93.png - Image Hash
      

      除了更具描述性之外,这种结构足以托管数十万张(取决于您的文件系统限制)几千年来每天的图像,这是就像 Wordpress 和其他人做的那样,我认为他们在这个上做对了。

      可以在数据库中轻松查询重复的图像,您只需创建符号链接。

      当然,如果这对您来说还不够,您可以随时添加更多子目录(小时、分钟、...)。

      我个人不会使用用户 ID,除非您的数据库中没有该信息,因为:

      1. 在 URL 中披露用户名
      2. 用户名不稳定(您也许可以重命名文件夹,但仍然...)
      3. 假设用户可以上传大量图片
      4. 毫无用处 (?)

      关于 CDN,我看不出这个方案(或任何其他方案)不起作用的任何原因......

      【讨论】:

        【解决方案5】:

        您可以将用户名转换为 md5,并将 md5 转换后的用户名的 2-3 个首字母设置为一个文件夹,用于头像和图像,您可以使用时间、随机字符串、ID 和名称进行转换和播放

        8648b8f3ce06a7cc57cf6fb931c91c55 - devcline

        也是下一个文件夹或反向文件夹的用户名或 ID 的第一个字母

        看起来像

        结构:

        stream/img/86/8b8f3ce06a7cc57cf6fb931c91c55.png    //simplest
        stream/img/d/2/0bbb630d63262dd66d2fdde8661a410075.png //first letter and id folders
        stream/img/864/d/8b8f3ce06a7cc57cf6fb931c91c55.png // with first letter of the nick
        stream/img/864/2/8b8f3ce06a7cc57cf6fb931c91c55.png   //with unique id
        stream/img/2864/8b8f3ce06a7cc57cf6fb931c91c55.png    //with unique id in 3 letters
        stream/img/864/2_8b8f3ce06a7cc57cf6fb931c91c55.png   //with unique id in picture name
        

        代码

        $username = substr($username_md5, 1); // to cut first letter from the md5 converted nick
        $username_first = $username[0]; // the first letter
        $username_md5 = md5($username); // md5 for username
        $randomname = uniqid($userid).md5(time());  //for generate a random name based on ID
        

        你也可以试试 base64

         $image_encode = strtr(base64_encode($imagename), '+/=', '-_,');
         $image_decode = base64_decode(strtr($imagename, '-_,', '+/='));
        

        Steam 和 dokuwiki 使用这种结构。

        【讨论】:

          【解决方案6】:

          是的,是的,我知道这是一个古老的话题。但是存储大量图像以及如何组织底层文件夹结构的问题。所以我提出了我的处理方法,希望这可以帮助一些人。

          使用 md5 哈希的想法是处理海量图像存储的最佳方式。请记住,不同的值可能具有相同的哈希值,我强烈建议将用户 ID 或 nicname 也添加到路径中以使其唯一。是的,这就是我们所需要的。如果某人有不同的用户具有相同的数据库 ID - 那么,有问题;)所以root_path/md5_hash/user_id 是你需要做的一切。

          顺便说一句,使用 DATE / DATETIME / TIMESTAMP 并不是最佳解决方案。在忙碌的一天,你会得到一大堆图像文件夹,而在不常去的时候,这些文件夹几乎是空的。不确定这会导致性能问题,但存在数据美学之类的问题,并且一致的数据分布始终是优越的。

          所以我显然选择了哈希解决方案。

          我编写了以下函数来轻松生成此类基于散列的存储路径。如果您喜欢,请随意使用它。

          /**
          * Generates directory path using $user_id md5 hash for massive image storing 
          * @author Hexodus 
          * @param string $user_id numeric user id
          * @param string $user_root_raw root directory string
          * @return null|string
          */
          
          function getUserImagePath($user_id = null, $user_root_raw = "images/users", $padding_length = 16, 
                                      $split_length = 3, $hash_length = 12, $hide_leftover = true)
          {
              // our db user_id should be nummeric
              if (!is_numeric($user_id))
                  return null;
          
              // clean trailing slashes  
              $user_root_rtrim = rtrim( $user_root_raw, '/\\' );
              $user_root_ltrim = ltrim( $user_root_rtrim, '/\\' );
              $user_root = $user_root_ltrim;
          
              $user_id_padded = str_pad($user_id, $padding_length, "0", STR_PAD_LEFT); //pad it with zeros  
              $user_hash = md5($user_id); // build md5 hash
          
              $user_hash_partial = $hash_length >=1 && $hash_length < 32 
                                  ? substr($user_hash, 0, $hash_length) : $user_hash;
              $user_hash_leftover = $user_hash_partial <= 32 ? substr($user_hash, $hash_length, 32) : null;
          
              $user_hash_splitted = str_split($user_hash_partial, $split_length); //split in chunks
              $user_hash_imploded = implode($user_hash_splitted,"/"); //glue aray chunks with slashes
          
              if ($hide_leftover || !$user_hash_leftover)
                  $user_image_path = "{$user_root}/{$user_hash_imploded}/{$user_id_padded}"; //build final path
              else
                  $user_image_path = "{$user_root}/{$user_hash_imploded}/{$user_hash_leftover}/{$user_id_padded}"; //build final path plus leftover
          
              return $user_image_path;
          }
          

          功能测试调用:

          $user_id = "1394";
          $user_root = "images/users"; 
          $user_hash = md5($user_id);
          $path_sample_basic = getUserImagePath($user_id);
          $path_sample_advanced = getUserImagePath($user_id, "images/users", 8, 4, 12, false);
          
          echo "<pre>hash: {$user_hash}</pre>";
          echo "<pre>basic:<br>{$path_sample_basic}</pre>";
          echo "<pre>customized:<br>{$path_sample_advanced}</pre>";
          echo "<br><br>";
          

          生成的输出 - 为了您的方便而着色;):

          【讨论】:

          • 很好的答案.. 绝对帮助我最好地理解散列存储。虽然你的分区散列在 users/ 之后不是有点长吗?如果它是 4 十六进制长(如 f016),这是否意味着可能有 15*15*15*15 (50625) 个文件夹可以存储在那里?如果它是 2 十六进制长 (f0),最大文件夹将是 15*15 (256)?这不是更可取吗?在您的原始图像中,您将 md5 哈希划分为 8 个长度为 4 十六进制的不同目录。这不是太过分了,浏览这么多子文件夹会影响性能吗?
          • @user3614030 很高兴我的回答对您有所帮助。如您所见,我还使用了通常是数据库中唯一 ID 的 ID,因此不需要完整长度的哈希。老实说,我不知道子文件夹是否会影响性能。
          【解决方案7】:

          我已经使用了很长时间的灵魂。这是相当老的代码,可以进一步优化,但它仍然可以很好地发挥作用。

          这是一个不可变的函数创建目录结构基于:

          1. 标识图像的编号(文件 ID):

          建议此数字对于基目录是唯一的,如数据库表的主键,但不是必需的。

          1. 基本目录

          2. 文件和第一级子目录的最大期望数量。这个承诺只有在每个 FILE ID 都是唯一的情况下才能兑现。

          使用示例:

          使用显式文件 ID:

          $fileName = 'my_image_05464hdfgf.jpg';
          $fileId = 65347;
          $baseDir = '/home/my_site/www/images/';
          $baseURL = 'http://my_site.com/images/';
          
          $clusteredDir = \DirCluster::getClusterDir( $fileId );
          $targetDir = $baseDir . $clusteredDir;
          $targetPath = $targetDir . $fileName;
          $targetURL = $baseURL . $clusteredDir  . $fileName;
          

          使用文件名,数字= crc32(文件名)

          $fileName = 'my_image_05464hdfgf.jpg';
          $baseDir = '/home/my_site/www/images/';
          $baseURL = 'http://my_site.com/images/';
          
          $clusteredDir = \DirCluster::getClusterDir( $fileName );
          $targetDir = $baseDir . $clusteredDir;
          $targetURL = $baseURL . $clusteredDir  . $fileName;
          

          代码:

          class DirCluster {
          
          
          /**
          * @param mixed $fileId       - numeric FILE ID or file name
          * @param int $maxFiles       - max files in one dir
          * @param int $maxDirs        - max 1st lvl subdirs in one dir
          * @param boolean $createDirs - create dirs?
          * @param string $path        - base path used when creatign dirs
          * @return boolean|string
          */
          public static function getClusterDir($fileId, $maxFiles = 100, $maxDirs = 10,
          $createDirs = false, $path = "") {
          
          // Value for return
          $rt = '';
          
          // If $fileId is not numerci - lets create crc32
          if (!is_numeric($fileId)) {
              $fileId = crc32($fileId);
          }
          
          if ($fileId < 0) {
            $fileId = abs($fileId);
          }
          
          if ($createDirs) {
          
              if (!file_exists($path))
              {
                  // Check out the rights - 0775 may be not the best for you
                  if (!mkdir($path, 0775)) { 
                    return false;
                  }
                  @chmod($path, 0775);
              }
          }
          
          if ( $fileId <= 0 || $fileId <= $maxFiles ) { 
            return $rt;
          }
          
          // Rest from dividing
          $restId = $fileId%$maxFiles;
          
          $formattedFileId = $fileId - $restId;
          
          // How many directories is needed to place file
          $howMuchDirs = $formattedFileId / $maxFiles;
          
          while ($howMuchDirs > $maxDirs)
          {
              $r = $howMuchDirs%$maxDirs;
              $howMuchDirs -= $r;
              $howMuchDirs = $howMuchDirs/$maxDirs;
              $rt .= $r . '/'; // DIRECTORY_SEPARATOR = /
          
              if ($createDirs)
              {
                  $prt = $path.$rt;
                  if (!file_exists($prt))
                  {
                      mkdir($prt);
                      @chmod($prt, 0775);
                  }
              }
          }
          
          $rt .= $howMuchDirs-1;
          if ($createDirs)
          {
              $prt = $path.$rt;
              if (!file_exists($prt))
              {
                  mkdir($prt);
                  @chmod($prt, 0775);
              }
          }
          
          $rt .= '/'; // DIRECTORY_SEPARATOR
          
          return $rt;
          
          
          }
          
          }
          

          【讨论】:

            猜你喜欢
            • 2013-10-25
            • 2016-06-19
            • 2012-12-10
            • 1970-01-01
            • 1970-01-01
            • 2017-04-06
            • 2020-10-05
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多