zoukankan      html  css  js  c++  java
  • 总结阿里云OSS的开发坑(Java篇)

    一、序言

    OSS(Object Storage Service)是阿里云提供的一款云存储服务,具有海量、安全、低成本、高可靠的特点。

    由于客户选择了OSS,我们作为开发方也开始接触它。在实际开发过程中遇到了各种各样的坑,经自己多次实践及阿里技术人员的协助,终得以完成任务。

    阿里方面为OSS提供了多种语言的开发接口,我们用到了其中两种:Java和C/C++。本文为Java篇,C/C++的将在另一篇给出。

    二、OSS的一些概念

    • EndPoint, accessKeyID, accessKeySecret:欲使用OSS,先要在阿里云上申请相应的空间资源,而EndPoint, accessKeyID, accessKeySecret则相当于域名、账号和密码,是所申请资源的使用凭证,需要妥善保管。
    • Bucket:是用于存储对象的容器,所有对象都必须属于且只属于一个Bucket,Bucket的属性(控制地域、访问权限、生命周期等)对所有对象都同等有效,同一空间资源下Bucket名必须唯一,且创建后不能再改名。
    • 对象/文件:对象/文件是 OSS 存储数据的基本单元。对象/文件由元信息(Object Meta),用户数据(Data)和文件名(Key)组成。文件名是唯一的,重复上传同名的对象意味着覆盖以前内容,但OSS支持在已有对象后部追加数据。
    • 目录:其实是一种特殊的对象(无Data),仅仅是为了管理方便,除此以外并无多大意义。
    • 其它概念,如分片、回调、追加、权限等,因开发中未涉及,不再展开,有兴趣者请参考:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.11186623.4.1.TamX1d

    三、1号坑:找不到对象抛出OSSException

    现在开始总结Java开发时的几个坑,这些都是SDK文档和示例代码中没有的。先从简单的说起。

    Java SDK提供了两种搜素OSS对象的方法:按文件名精确匹配和按文件前缀批量查找,不支持其它模糊查询、正规表达式查询,也不支持按元信息查询。这些都好理解,不多说。

    在按文件名Key精确查找OSS对象时,如果不存在该Key对应的文件,会抛出OSSException(对应的错误信息是ErrorCode为“NoSuchKey”),而不是返回null。

    因此,需要在Java代码里加上对该例外的捕获处理:

        try
        {
            OSSObject obj = null;
            obj = client.getObject(bucketName, ossKey);
            if (obj != null)
              obj.close();
        }
        catch (OSSException e)
        {
           // OSS在查找不到某对象时,会抛出ErrorCode为“NoSuchKey”的OSSException,而不是返回null
           if (e.getErrorCode().contains("NoSuchKey"))
           {
              System.out.println("找不到OSS文件:" + ossKey);
              continue;
           }
           else
           e.printStackTrace();
        }
        catch (ClientException | IOException e)
        {
           e.printStackTrace();
        } 

    四、2号坑:分页遍历时的死循环

    OSS Java SDK提供了分页遍历的功能,一页最多可以遍历1000个文件。但如果一边遍历一边更新对象,则很容易形成死循环。

    项目中的一个小工具的示例代码如下:

            String nextMarker = null;
            ObjectListing objListing;
            do
            {
                if (nextMarker == null) // 第一次的分页
                    objListing = client.listObjects(new ListObjectsRequest(bucketName).withMaxKeys(1000));
                else // 以后的分页,附带nextMarker
                    objListing = client.listObjects(
                            new ListObjectsRequest(bucketName).withMarker(nextMarker).withMaxKeys(1000));
    
                List<OSSObjectSummary> sums = objListing.getObjectSummaries();
                for (OSSObjectSummary s : sums)
                {
                    String ossKey = s.getKey();
                    ...
                    ossClient.putObject(bucketName, s, new ByteArrayInputStream(mnt.getData()));
                    ...
                }
                // 下一次分页的nextMarker
                nextMarker = objListing.getNextMarker();
             } while (objListing.isTruncated());

    因为有了putObject操作(带颜色处),运行时成了死循环,objListing.isTruncated()永远为false。

    经测试,死循环不仅仅出现在如上的一段代码中既遍历又修改的情况下,一个进程循环写的同时另一个进程分页遍历也会出现。猜测遍历的依据条件主要是修改时间,但没法区分已经遍历过的对象。

    貌似没有特别好的解决办法,我们使用的是一种土办法:选定一个特殊对象,再次遍历到它即强行退出循环。

    五、3号坑:循环getObject超过1024次的挂起

    有一个操作需要批量读取OSS对象,按示例代码编写后测试,发现一旦循环调用getObject()程序就会挂起,不继续运行也不退出,只能强行关闭。

    后来经一步步跟踪,发现此问题是由于getObject()后没有及时close对象而引起,临界值是1024(也可能是1023)。

            List<String> pks; //存放的是ossKey
            for (String pk : pks)
            {
                HSBJMntDataPK pk = pks.get(i);
    
                OSSObject obj = null;
                try
                {
                    obj = ossClient.getObject(bucketName, pk);
                }
                catch (OSSException e)
                {
                    if (e.getErrorCode().contains("NoSuchKey"))
                        continue;
                    e.printStackTrace();
                }
                catch (ClientException e)
                {
                    e.printStackTrace();
                }
    
                // 处理obj的代码,略过
    
                try // 及时释放OSSObject,否则循环达到1024次会suspend
                {
                    obj.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }

    增加了颜色标出的obj.close()后,不再发生程序挂起的现象。

    六、4号坑:关于UserMetaData中的大小写

     如果OSS对象带有一些简单的自定义属性,比如本项目中用到的创建者、版本、类型、备注等,可以作为UserMetaData存放到元信息(Object Meta)中。与把它们合并到Data中的方式相比,这样做不但简化了Data的构造和解析过程,还可以缩短读写时间,在本项目中能提高速度30%左右。

    但是,一开始把属性值写入元信息后,读取时却读不到值。后来发现,UserMetaData的key值按照项目习惯是首字母大写的,但在写入时OSS都自动转换为全小写处理,读取时再按首字母大写就读取不到。在将UserMetaData的key值改为全小写后,问题解决。

        public static ObjectMetadata buildMeta(MntData mnt)
        {
            ObjectMetadata meta = new ObjectMetadata();
            // UserMetadata中,key会被转换为全部小写,所以为统一赋值时也用小写
            meta.addUserMetadata("author", author);
            meta.addUserMetadata("version", version);
            meta.addUserMetadata("type", type);
            meta.addUserMetadata("purpose", purpose);
            return meta;
        }
    
        public static MntData parseObject(OSSObject ossObject)
        {
            ObjectMetadata meta = ossObject.getObjectMetadata();
            Map<String, String> metadata = meta.getUserMetadata();
            MntData mnt = new MntData();
            // 从UserMeta获取value时,key必须为全小写
            mnt.setAuthor(metadata.get("author") == null ? "-" : metadata.get("author"));
            mnt.setVersion(metadata.get("version") == null ? "1.0" : metadata.get("version"));
            mnt.setType(metadata.get("type") == null ? "-" : metadata.get("type"));
            mnt.setPurpose(metadata.get("purpose") == null ? "" : metadata.get("purpose"));
            // UpdateTime为OSS自带的元数据
            mnt.setUpdateTime(new Timestamp(meta.getLastModified().getTime()));
    
             mnt.setData(getBytesFromObj(ossObject));
    
            return mnt;
        }

    七、经验:使用多进程可提升速度

    注意这不是坑,而是一条有益经验:无论是读还是写,使用多线程可显著提升批量操作的速度。

    以下是写OSS进程的示例代码:

    public class OssPutThread extends Thread
    {
        List<MntData> mnts;
        int index;
    
        public OssPutThread(List<MntData> mnts, int index)
        {
            this.mnts = mnts;
            this.index = index;
        }
    
        @Override
        public void run()
        {
            // 线程内新生成一个OSSClient
            OSSClient ossClient = new OSSClient(endPoint, accessKeyId, accessKeySecret);
            for (int i = 0; i < mnts.size(); i ++)
            {
                HSBJMntData mnt = mnts.get(i);
                String ossKey = OssUtil.buildOssKey(mnt); 
                // data转换为InputStream,其它属性值放入ObjectMetaData
                ossClient.putObject(bucketName, ossKey, OssUtil.buildOssObject(mnt), OssUtil.buildMeta(mnt));
            }
    
            //线程结束前释放OSSClient
            ossClient.shutdown();
        }
    }

    在本项目中,线程数每多一倍,批量读写的速度可提升90%,效果相当明显。

  • 相关阅读:
    Beyond Compare保存快照和CRC比较相结合的方法
    如何在Beyond Compare文本比较时设置书签
    如何使用Navicat for SQLite 触发器
    Navicat Premium 中实用工具介绍
    Beyond Compare查看合并文本后相同内容的方法
    Marriage Match II HDU
    Escape HDU
    kebab HDU
    Task Schedule HDU
    网络流深入
  • 原文地址:https://www.cnblogs.com/wggj/p/9172202.html
Copyright © 2011-2022 走看看