前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[数据库连接池] Java数据库连接池--DBCP浅析.

[数据库连接池] Java数据库连接池--DBCP浅析.

作者头像
一枝花算不算浪漫
发布2018-05-18 11:03:40
1.5K0
发布2018-05-18 11:03:40
举报
文章被收录于专栏:一枝花算不算浪漫的专栏

前言 对于数据库连接池, 想必大家都已经不再陌生, 这里仅仅设计Java中的两个常用数据库连接池: DBCP和C3P0(后续会更新).  一. 为何要使用数据库连接池 假设网站一天有很大的访问量,数据库服务器就需要为每次连接创建一次数据库连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、拓机。 数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池正式针对这个问题提出来的.数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中, 这些数据库连接的数量是由最小数据库连接数来设定的.无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量.连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中.

数据库连接池的最小连接数和最大连接数的设置要考虑到以下几个因素:

  1, 最小连接数:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费.   2, 最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作   3, 如果最小连接数与最大连接数相差很大:那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接.不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,他将被           放到连接池中等待重复使用或是空间超时后被释放. 二, 数据库连接池的原理及实现 到了这里我们已经知道数据库连接池是用来做什么的了, 下面我们就来说数据库连接池是如何来实现的.  1, 建立一个数据库连接池pool, 池中有若干个Connection 对象, 当用户发来请求需要进行数据库交互时则会使用池中第一个Connection对象. 2, 当本次连接结束时, 再将这个Connection对象归还池中, 这样就可以保证池中一直有足够的Connection对象.

代码语言:javascript
复制
public class SimplePoolDemo {
    //创建一个连接池
    private static LinkedList<Connection> pool = new LinkedList<Connection>(); 
    
    //初始化10个连接
    static{
        try {
            for (int i = 0; i < 10; i++) {
                Connection conn = DBUtils.getConnection();//得到一个连接
                pool.add(conn);
            }
        } catch (Exception e) {
            throw new ExceptionInInitializerError("数据库连接失败,请检查配置");
        }
    }
    //从池中获取一个连接
    public static Connection getConnectionFromPool(){
        return pool.removeFirst();//移除一个连接对象
    }
    //释放资源
    public static void release(Connection conn){
        pool.addLast(conn);
    }
}

以上的Demo就是一个简单的数据库连接池的例子, 先在静态代码块中初始化10个Connection对象, 当本次请求结束后再将Connection添加进池中.  这只是我们自己手动去实现的, 当然在实际生产中并不需要我们去手动去写数据库连接池. 下面就重点讲DBCP和C3P0的实现方式. 三, DBCP连接池 首先我们来看DBCP 的例子, 然后根据例子来分析:

代码语言:javascript
复制
 1 #连接设置
 2 driverClassName=com.mysql.jdbc.Driver
 3 url=jdbc:mysql://localhost:3306/day14
 4 username=root
 5 password=abc
 6 
 7 #<!-- 初始化连接 -->
 8 initialSize=10
 9 
10 #最大连接数量
11 maxActive=50
12 
13 #<!-- 最大空闲连接 -->
14 maxIdle=20
15 
16 #<!-- 最小空闲连接 -->
17 minIdle=5
18 
19 #<!-- 超时等待时间以毫秒为单位 60000毫秒/1000等于60秒 -->
20 maxWait=60000
21 
22 
23 #JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;] 
24 #注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
25 connectionProperties=useUnicode=true;characterEncoding=utf8
26 
27 #指定由连接池所创建的连接的自动提交(auto-commit)状态。
28 defaultAutoCommit=true
29 
30 #driver default 指定由连接池所创建的连接的只读(read-only)状态。
31 #如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
32 defaultReadOnly=
33 
34 #driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
35 #可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
36 defaultTransactionIsolation=REPEATABLE_READ

DBCPUtils:

代码语言:javascript
复制
 1 public class DBCPUtils {
 2     private static DataSource ds;//定义一个连接池对象
 3     static{
 4         try {
 5             Properties pro = new Properties();
 6             pro.load(DBCPUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties"));
 7             ds = BasicDataSourceFactory.createDataSource(pro);//得到一个连接池对象
 8         } catch (Exception e) {
 9             throw new ExceptionInInitializerError("初始化连接错误,请检查配置文件!");
10         }
11     }
12     //从池中获取一个连接
13     public static Connection getConnection() throws SQLException{
14         return ds.getConnection();
15     }
16     
17     public static void closeAll(ResultSet rs,Statement stmt,Connection conn){
18         if(rs!=null){
19             try {
20                 rs.close();
21             } catch (SQLException e) {
22                 e.printStackTrace();
23             }
24         }
25         
26         if(stmt!=null){
27             try {
28                 stmt.close();
29             } catch (SQLException e) {
30                 e.printStackTrace();
31             }
32         }
33         
34         if(conn!=null){
35             try {
36                 conn.close();//关闭
37             } catch (SQLException e) {
38                 e.printStackTrace();
39             }
40         }
41     }
42 }

在这个closeAll方法中, conn.close(); 这个地方会将connection还回到池子中吗? DataSource 中是如何处理close()方法的呢? 上面的两个问题就让我们一起来看看源码是如何来实现的吧. 这里我们从ds.getConnection();入手, 看看一个数据源DataSource是如何创建connection的. 用eclipse导入:commons-dbcp-1.4-src.zip和commons-pool-1.5.6-src.zip则可查看源码: BasicDataSource.class:(implements DataSource)

代码语言:javascript
复制
public Connection getConnection() throws SQLException {
      return createDataSource().getConnection();
}

3.1 接下来看createDataSoruce() 方法:

代码语言:javascript
复制
 1 protected synchronized DataSource createDataSource()
 2     throws SQLException {
 3     if (closed) {
 4         throw new SQLException("Data source is closed");
 5     }
 6 
 7     // Return the pool if we have already created it
 8     if (dataSource != null) {
 9         return (dataSource);
10     }
11 
12     // create factory which returns raw physical connections
13     ConnectionFactory driverConnectionFactory = createConnectionFactory();
14 
15     // create a pool for our connections
16     createConnectionPool();
17 
18     // Set up statement pool, if desired
19     GenericKeyedObjectPoolFactory statementPoolFactory = null;
20     if (isPoolPreparedStatements()) {
21         statementPoolFactory = new GenericKeyedObjectPoolFactory(null,
22                     -1, // unlimited maxActive (per key)
23                     GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL,
24                     0, // maxWait
25                     1, // maxIdle (per key)
26                     maxOpenPreparedStatements);
27     }
28 
29     // Set up the poolable connection factory
30     createPoolableConnectionFactory(driverConnectionFactory, statementPoolFactory, abandonedConfig);
31 
32     // Create and return the pooling data source to manage the connections
33     createDataSourceInstance();
34     
35     try {
36         for (int i = 0 ; i < initialSize ; i++) {
37             connectionPool.addObject();
38         }
39     } catch (Exception e) {
40         throw new SQLNestedException("Error preloading the connection pool", e);
41     }
42     
43     return dataSource;
44 }

从源代码可以看出,createDataSource()方法通过7步,逐步构造出一个数据源,下面是详细的步骤:

   1、检查数据源是否关闭或者是否创建完成,如果关闭了就抛异常,如果已经创建完成就直接返回。

   2、调用createConnectionFactory()创建JDBC连接工厂driverConnectionFactory,这个工厂使用数据库驱动来创建最底层的JDBC连接

   3、调用createConnectionPool()创建数据源使用的连接池,连接池顾名思义就是缓存JDBC连接的地方。

   4、如果需要就设置statement的缓存池,这个一般不需要设置

   5、调用createPoolableConnectionFactory创建PoolableConnection的工厂,这个工厂使用上述driverConnectionFactory来创建底层JDBC连接,然后包装出一个PoolableConnection,这个PoolableConnection与连接池设置了一对多的关系,也就是说,连接池中存在多个PoolableConnection,每个PoolableConnection都关联同一个连接池,这样的好处是便于该表PoolableConnection的close方法的行为,具体会在后面详细分析。

   6、调用createDataSourceInstance()创建内部数据源

   7、为连接池中添加PoolableConnection

经过以上7步,一个数据源就形成了,这里明确一点,一个数据源本质就是连接池+连接+管理策略。下面,将对每一步做详细的分析。 3.2 JDBC连接工厂driverConnectionFactory的创建过程

代码语言:javascript
复制
 1 protected ConnectionFactory createConnectionFactory() throws SQLException {
 2     // Load the JDBC driver class
 3     Class driverFromCCL = null;
 4     if (driverClassName != null) {
 5         try {
 6             try {
 7                 if (driverClassLoader == null) {
 8                     Class.forName(driverClassName);
 9                 } else {
10                     Class.forName(driverClassName, true, driverClassLoader);
11                 }
12             } catch (ClassNotFoundException cnfe) {
13                 driverFromCCL = Thread.currentThread(
14                         ).getContextClassLoader().loadClass(
15                                 driverClassName);
16             }
17         } catch (Throwable t) {
18             String message = "Cannot load JDBC driver class '" +
19                 driverClassName + "'";
20             logWriter.println(message);
21             t.printStackTrace(logWriter);
22             throw new SQLNestedException(message, t);
23         }
24     }
25 
26     // Create a JDBC driver instance
27     Driver driver = null;
28     try {
29         if (driverFromCCL == null) {
30             driver = DriverManager.getDriver(url);
31         } else {
32             // Usage of DriverManager is not possible, as it does not
33             // respect the ContextClassLoader
34             driver = (Driver) driverFromCCL.newInstance();
35             if (!driver.acceptsURL(url)) {
36                 throw new SQLException("No suitable driver", "08001"); 
37             }
38         }
39     } catch (Throwable t) {
40         String message = "Cannot create JDBC driver of class '" +
41             (driverClassName != null ? driverClassName : "") +
42             "' for connect URL '" + url + "'";
43         logWriter.println(message);
44         t.printStackTrace(logWriter);
45         throw new SQLNestedException(message, t);
46     }
47 
48     // Can't test without a validationQuery
49     if (validationQuery == null) {
50         setTestOnBorrow(false);
51         setTestOnReturn(false);
52         setTestWhileIdle(false);
53     }
54 
55     // Set up the driver connection factory we will use
56     String user = username;
57     if (user != null) {
58         connectionProperties.put("user", user);
59     } else {
60         log("DBCP DataSource configured without a 'username'");
61     }
62 
63     String pwd = password;
64     if (pwd != null) {
65         connectionProperties.put("password", pwd);
66     } else {
67         log("DBCP DataSource configured without a 'password'");
68     }
69 
70     ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driver, url, connectionProperties);
71     return driverConnectionFactory;
72 }

上面一连串代码干了什么呢?其实就干了两件事:1、获取数据库驱动 2、使用驱动以及参数(url、username、password)构造一个工厂。一旦这个工厂构建完毕了,就可以来生成连接,而这个连接的生成其实是驱动加上配置来完成的. 3.3 创建连接池的过程

代码语言:javascript
复制
 1 protected void createConnectionPool() {
 2         // Create an object pool to contain our active connections
 3         GenericObjectPool gop;
 4         if ((abandonedConfig != null) && (abandonedConfig.getRemoveAbandoned())) {
 5             gop = new AbandonedObjectPool(null,abandonedConfig);
 6         }
 7         else {
 8             gop = new GenericObjectPool();
 9         }
10         gop.setMaxActive(maxActive);
11         gop.setMaxIdle(maxIdle);
12         gop.setMinIdle(minIdle);
13         gop.setMaxWait(maxWait);
14         gop.setTestOnBorrow(testOnBorrow);
15         gop.setTestOnReturn(testOnReturn);
16         gop.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
17         gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
18         gop.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
19         gop.setTestWhileIdle(testWhileIdle);
20         connectionPool = gop;
21     }

在创建连接池的时候,用到了common-pool里的GenericObjectPool,对于JDBC连接的缓存以及管理其实是交给GenericObjectPool的,DBCP其实只是负责创建这样一种pool然后使用它而已。 3.4 创建statement缓存池 一般来说,statement并不是重量级的对象,创建过程消耗的资源并不像JDBC连接那样重,所以没必要做缓存池化,这里为了简便起见,对此不做分析。 3.5 创建PoolableConnectionFactory

这一步是一个承上启下的过程,承上在于利用上面两部创建的连接工厂和连接池,构建PoolableConnectionFactory,启下则在于为后面的向连接池里添加连接做准备。    下面先上一张静态的类关系图:

代码语言:javascript
复制
 1 xprotected void createPoolableConnectionFactory(ConnectionFactory driverConnectionFactory,
 2         KeyedObjectPoolFactory statementPoolFactory, AbandonedConfig configuration) throws SQLException {
 3     PoolableConnectionFactory connectionFactory = null;
 4     try {
 5         connectionFactory =
 6             new PoolableConnectionFactory(driverConnectionFactory,
 7                                           connectionPool,
 8                                           statementPoolFactory,
 9                                           validationQuery,
10                                           validationQueryTimeout,
11                                           connectionInitSqls,
12                                           defaultReadOnly,
13                                           defaultAutoCommit,
14                                           defaultTransactionIsolation,
15                                           defaultCatalog,
16                                           configuration);
17         validateConnectionFactory(connectionFactory);
18     } catch (RuntimeException e) {
19         throw e;
20     } catch (Exception e) {
21         throw new SQLNestedException("Cannot create PoolableConnectionFactory (" + e.getMessage() + ")", e);
22     }
23 }

可以看见,在创建PoolableConnectionFactory的时候,需要用到前面创建的driverConnectionFactory以及连接池connectionPool,那么那个构造函数到底干了先什么呢?

代码语言:javascript
复制
 1 public PoolableConnectionFactory(
 2     ConnectionFactory connFactory,
 3     ObjectPool pool,
 4     KeyedObjectPoolFactory stmtPoolFactory,
 5     String validationQuery,
 6     int validationQueryTimeout,
 7     Collection connectionInitSqls,
 8     Boolean defaultReadOnly,
 9     boolean defaultAutoCommit,
10     int defaultTransactionIsolation,
11     String defaultCatalog,
12     AbandonedConfig config) {
13 
14     _connFactory = connFactory;
15     _pool = pool;
16     _config = config;
17     _pool.setFactory(this);
18     _stmtPoolFactory = stmtPoolFactory;
19     _validationQuery = validationQuery;
20     _validationQueryTimeout = validationQueryTimeout;
21     _connectionInitSqls = connectionInitSqls;
22     _defaultReadOnly = defaultReadOnly;
23     _defaultAutoCommit = defaultAutoCommit;
24     _defaultTransactionIsolation = defaultTransactionIsolation;
25     _defaultCatalog = defaultCatalog;
26 }

 它在内部保存了真正的JDBC 连接的工厂以及连接池,然后,通过一句_pool.setFactory(this); 将它自己设置给了连接池。这行代码十分重要,要理解这行代码,首先需要明白common-pool中的GenericObjectPool添加内部元素的一般方法,没错,那就是必须要传入一个工厂Factory。GenericObjectPool添加内部元素时会调用addObject()这个方法,内部其实是调用工厂的makeObejct()方法来创建元素,然后再加入到自己的池中。_pool.setFactory(this)这句代码其实起到了启下的作用,没有它,后面的为连接池添加连接也就不可能完成。

   当创建完工厂后,会有个validateConnectionFactory(connectionFactory);这个方法的作用仅仅是用来验证数据库连接可使用,看代码:

代码语言:javascript
复制
 1 protected static void validateConnectionFactory(PoolableConnectionFactory connectionFactory) throws Exception {
 2     Connection conn = null;
 3     try {
 4         conn = (Connection) connectionFactory.makeObject();
 5         connectionFactory.activateObject(conn);
 6         connectionFactory.validateConnection(conn);
 7         connectionFactory.passivateObject(conn);
 8     }
 9     finally {
10         connectionFactory.destroyObject(conn);
11     }
12 }

先是用makeObject方法来创建一个连接,然后做相关验证(就是用一些初始化sql来试着执行一下,看看能不能连接到数据库),然后销毁连接,这里并没有向连接池添加连接,真正的添加连接在后面,不过,我们可以先通过下面一张时序图来看看makeObject方法到底做了什么。

下面是一张整体流程的时序图:

从图中可以看出,makeObject方法的大致流程:从driverConnectionFactory那里拿到底层连接,初始化验证,然后创建PoolableConnection,在创建这个PoolableConnection的时候,将PoolableConnection与连接池关联了起来,真正做到了连接池和连接之间的一对多的关系,这也为改变PoolableConnection的close方法提供了方便。

下面是makeObject方法的源代码:

代码语言:javascript
复制
 1 public Object makeObject() throws Exception {
 2     Connection conn = _connFactory.createConnection();
 3     if (conn == null) {
 4         throw new IllegalStateException("Connection factory returned null from createConnection");
 5     }
 6     initializeConnection(conn); //初始化,这个过程可有可无
 7     if(null != _stmtPoolFactory) {  
 8         KeyedObjectPool stmtpool = _stmtPoolFactory.createPool();
 9         conn = new PoolingConnection(conn,stmtpool);
10         stmtpool.setFactory((PoolingConnection)conn);
11     }
12     //这里是关键
13     return new PoolableConnection(conn,_pool,_config); 
14 }

其中PoolableConnection的构造函数如下:

代码语言:javascript
复制
public PoolableConnection(Connection conn, ObjectPool pool, AbandonedConfig config) {
    super(conn, config);
    _pool = pool;
}

内部关联了一个连接池,这个连接池的作用体现在PoolableConnection的close方法中:

代码语言:javascript
复制
 1 public synchronized void close() throws SQLException {
 2     if (_closed) {
 3         // already closed
 4         return;
 5     }
 6 
 7     boolean isUnderlyingConectionClosed;
 8     try {
 9         isUnderlyingConectionClosed = _conn.isClosed();
10     } catch (SQLException e) {
11         try {
12             _pool.invalidateObject(this); // XXX should be guarded to happen at most once
13         } catch(IllegalStateException ise) {
14             // pool is closed, so close the connection
15             passivate();
16             getInnermostDelegate().close();
17         } catch (Exception ie) {
18             // DO NOTHING the original exception will be rethrown
19         }
20         throw (SQLException) new SQLException("Cannot close connection (isClosed check failed)").initCause(e);
21     }
22 
23     if (!isUnderlyingConectionClosed) {
24         // Normal close: underlying connection is still open, so we
25         // simply need to return this proxy to the pool
26         try {
27             _pool.returnObject(this); // XXX should be guarded to happen at most once
28         } catch(IllegalStateException e) {
29             // pool is closed, so close the connection
30             passivate();
31             getInnermostDelegate().close();
32         } catch(SQLException e) {
33             throw e;
34         } catch(RuntimeException e) {
35             throw e;
36         } catch(Exception e) {
37             throw (SQLException) new SQLException("Cannot close connection (return to pool failed)").initCause(e);
38         }
39     } else {
40         // Abnormal close: underlying connection closed unexpectedly, so we
41         // must destroy this proxy
42         try {
43             _pool.invalidateObject(this); // XXX should be guarded to happen at most once
44         } catch(IllegalStateException e) {
45             // pool is closed, so close the connection
46             passivate();
47             getInnermostDelegate().close();
48         } catch (Exception ie) {
49             // DO NOTHING, "Already closed" exception thrown below
50         }
51         throw new SQLException("Already closed.");
52     }
53 }

一行_pool.returnObject(this)表明并非真的关闭了,而是返还给了连接池。

 到这里, PoolableConnectionFactory创建好了,它使用driverConnectionFactory来创建底层连接,通过makeObject来创建PoolableConnection,这个PoolableConnection通过与connectionPool关联来达到改变close方法的作用,当PoolableConnectionFactory创建好的时候,它自己已经作为一个工厂类被设置到了connectionPool,后面connectionPool会使用这个工厂来生产PoolableConnection,而生成的所有的PoolableConnection都与connectionPool关联起来了,可以从connectionPool取出,也可以还给connectionPool。接下来,让我们来看一看到底怎么去初始化connectionPool。 3.6 创建数据源并初始化连接池

代码语言:javascript
复制
createDataSourceInstance();
    
try {
   for (int i = 0 ; i < initialSize ; i++) {
       connectionPool.addObject();
  }
 } catch (Exception e) {
        throw new SQLNestedException("Error preloading the connection pool", e);
 }

我们先看 createDataSourceInstance();

代码语言:javascript
复制
protected void createDataSourceInstance() throws SQLException {
    PoolingDataSource pds = new PoolingDataSource(connectionPool);
    pds.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed());
    pds.setLogWriter(logWriter);
    dataSource = pds;
}

其实就是创建一个PoolingDataSource,作为底层真正的数据源,这个PoolingDataSource比较简单,这里不做详细介绍

接下来是一个for循环,通过调用connectionPool.addObject();来为连接池添加数据库连接,下面是一张时序图:

可以看出,在3.5中创建的PoolableConnectionFactory在这里起作用了,addObject依赖的正是makeObject,而makeObject在上面也介绍过了。

到此为止,数据源创建好了,连接池里也有了可以使用的连接,而且每个连接和连接池都做了关联,改变了close的行为。这个时候BasicDataSource正是可以工作了,调用getConnection的时候,实际是调用底层数据源的getConnection,而底层数据源其实就是从连接池中获取的连接。 四.总结

 整个数据源最核心的其实就三个东西:一个是连接池,在这里体现为common-pool中的GenericObjectPool它负责缓存和管理连接,所有的配置策略都是由它管理第二个是连接,这里的连接就是PoolableConnection,当然它是对底层连接进行了封装。第三个则是连接池和连接的关系,在此表现为一对多的互相引用。对数据源的构建则是对连接池,连接以及连接池与连接的关系的构建,掌握了这些点,就基本能掌握数据源的构建。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-05-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档