级别: 初级 Suresh Thalamati (tsuresh@us.ibm.com), 软件工程师, IBM Cloudscape Group
2005 年 3 月 01 日 确保 Cloudscape 数据的可恢复性。了解 IBM® Cloudscape™ 和 Apache Derby 中备份、恢复和前滚恢复这些特性的工作原理。通过例子,学习如何恢复数据库。
简介
对于任何数据库管理系统来说,备份和恢复功能是确保数据可恢复性的关键。应用程序可能会崩溃,磁盘可能出故障,用户也经常会犯错误,以致损坏数据。由于这些原因,理解数据库管理系统中备份和恢复功能的工作原理,并实施经过精心规划的备份策略,就显得至关重要了。
注意:IBM Cloudscape 是开放源代码 Apache Derby 关系数据库的商业版本。当本文引用 "Derby" 时,它指的是 Cloudscape 和 Apache Derby 中的某一个。
Derby 提供了两种恢复机制:
- 一种是使用数据库的全备份,使数据库返回到备份时的状态。
- 另一种机制通过使用全备份,并前滚事务日志,使数据库恢复到最近的状态,这种机制也叫前滚恢复。
数据库备份可以通过使用系统过程在线执行,也可以使用操作系统复制命令离线执行。恢复操作是通过连接 URL 属性提供的。可以从 IJ(Derby 命令行工具),或者通过一个 Java Database Connectivity (JDBC) 应用程序启动备份或恢复。嵌入式配置和网络服务器配置都支持备份和恢复操作。如果要从网络服务器客户机执行备份/恢复,那么应确保可以从网络服务器客户机访问所指定的备份位置。
在线备份
当数据库系统在运行时,备份将通过使用 SYSCS_UTIL.SYSCS_BACKUP_DATABASE(IN BACKUPDIR VARCHAR(32762)) 系统过程在线执行。为了执行这个备份过程,必须有到所备份数据库的一个连接。
 |
什么是事务日志?
事务日志存储着对数据库作出的更改,当停电或者应用程序崩溃的时候,为了从备份恢复数据库,就需要事务日志。Derby 将事务日志以文件的形式按顺序存放在文件系统中,事务日志文件的文件名都有前缀 log 以及后缀 dat ,另外,前缀的后面还会加上一个数字,每当创建新的事务日志文件时这个数据就会递增,例如 log1.dat、log2.dat,等等。事务日志文件存储在数据库目录下的 log 目录中,或者存储在用户指定的一个位置。
|
|
在 Derby 中,在进行备份的时候,那些要求对磁盘进行写操作的语句,例如 insert、delete 和 update,将被阻塞。而那些只需要读磁盘的语句,例如 select,则不会被阻塞。一旦备份完成,系统将让所有被阻塞的语句完成它们的执行。在线备份可能会降低系统的吞吐量和增加用户响应时间,因此,应该尽可能在更新操作比较少的时候执行备份。
清单 1: 通过 IJ 对 salesdb 数据库进行备份
ij>connect 'jdbc:derby:salesdb';
ij>CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE('D:/dbbackups/');
|
嵌入式应用程序可以使用 JDBC API 来执行备份,而应用程序客户机不需要管理备份。在 Derby 中,所有系统过程也都可以通过 JDBC API 来调用。
清单 2: 使用 JDBC 调用执行备份
private void backUpDatabase(Connection conn) throws SQLException
{
String sqlstmt = "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)";
CallableStatement cs = conn.prepareCall(sqlstmt);
cs.setString(1,"D:/dbbackups/");
cs.execute();
cs.close();
}
|
该备份在指定的备份位置创建一个目录,目录名与数据库名相同。这个目录包含数据库目录中的所有文件/目录。数据库目录通常有一个数据目录(seg0)来存储用于表和索引的所有数据文件,有一个事务日志目录(log)来存储所有的事务日志文件,还有一个 service.properties 文件,该文件包含用于启动数据库的信息。
离线备份
当系统离线时,可以使用操作系统命令复制整个数据库目录。如果事务日志目录与数据库目录有不同之处,那么应确保同时复制了事务日志目录。在执行恢复操作时,需要使用事务日志使数据库处于一致状态。
也可以在数据库系统没有离线的情况下执行离线备份,方法是当复制数据库目录的时候冻结系统(阻塞所有写操作)。这里所用的系统过程是 SYSCS_UTIL.SYSCS_FREEZE_DATABASE() 。一旦复制完成,就可以使用 SYSCS_UTIL.SYSCS_UNFREEZE_DATABASE() 过程解冻系统(允许写操作)。该过程调用将把系统设置回正常状态。
使用备份进行恢复
数据库的在线/离线备份镜像可用于将数据库恢复到备份时所处的状态。通过在到数据库的第一个连接的连接 URL 中指定属性 restoreFrom=<backup path> ,可以从备份中恢复数据库。注意,backup path 必须包括备份中的数据库名,而不只是包含备份路径。数据库将被恢复到连接 URL 中指定的位置,或者,如果在连接 URL 中没有指定路径,则恢复到当前 derby.system.home 位置。如果在当前数据库位置已经有一个具有相同名称的数据库,那么首先会删除这个数据库,然后再从备份恢复数据库。
清单 3: 从备份恢复 salesdb 数据库:
String dbURL = "jdbc:derby:salesdb;restoreFrom=D:/dbbackups/salesdb";
Connection conn = DriverManager.getConnection(dbURL);
|
默认情况下,事务日志将被复制回当初备份时所在位置。假如存放事务日志的磁盘有问题,也可以修改事务日志的存放位置。为此,可以指定连接 URL 属性 logDevice=<log dir> 。
清单 4: 从备份恢复 salesdb 数据库并将事务日志放在不同的位置:
String dbURL = "jdbc:derby:salesdb;restoreFrom=D:/dbbackups/saleddb;logDevice=E:/salesdbLog";
Connection conn = DriverManager.getConnection(dbURL);
|
转移或克隆数据库
在 Derby 中,将数据库从一个系统转移到另一个系统,克隆测试用的数据库,以及增大数据库,这些都很容易。在第一次连接到数据库时,通过使用连接 URL 属性 createFrom=<backup path> ,可以从备份重建数据库。数据库将被创建在连接 URL 中指定的位置上,或者,如果连接 URL 中没有指定位置,则创建在当前 derby.system.home 位置上。默认情况下,事务日志存放在数据库目录下,可以通过在连接 URL 中指定 logDevice=<log dir > 属性来更改事务日志位置。
清单 5: 使用备份镜像克隆 salesdb 数据库
String dbURL = "jdbc:derby:salesdb;createFrom=D:/dbbackups/salesdb;logDevice=E:/salesdbLog";
Connection conn = DriverManager.getConnection(dbURL);
|
对于 Derby,可以从任何支持 Java 的操作系统或硬件上重建数据库。新的系统不一定需要与备份时数据库所在的系统拥有相同的配置。
前滚恢复
仅仅使用在线/离线备份镜像,可以将数据库恢复到采取备份时所处的状态。但是,通过使用前滚恢复,可以将数据库恢复到最近的状态。前滚恢复首先从备份做一次恢复,然后应用自进行备份起记录的所有事务日志记录,将数据库恢复到最近的状态。执行前滚恢复所需的信息有:
- 完全在线备份镜像。
- 备份之后的所有事务日志文件。
默认情况下,在停电或者应用程序崩溃时,如果要进行恢复,只需要系统保留的事务日志文件即可,其他所有事务日志文件会在检查点操作成功时被删除。为了可以选择执行前滚恢复,在备份后生成的所有事务日志文件都需要归档。为此,在备份时,可以启用日志归档模式。这里可以使用的系统过程是:
SYSCS_UTIL.SYSCS_BACKUP_DATABASE_AND_ENABLE_LOG_ARCHIVE_MODE(IN BACKUPDIR
VARCHAR(32762), IN SMALLINT DELETE_ARCHIVED_LOG_FILES)
。该过程将执行备份,启用事务日志归档模式,如果传给 DELETE_ARCHIVED_LOG_FILES 参数的值不是零,它还将删除为了从以前的备份进行恢复而归档的事务日志文件。
所有归档的事务日志文件也存储在数据库事务日志目录中。目前,Derby 还不支持将归档的事务日志放到不同的位置。重要的是,应确保使用某种容错机制,例如镜像,使包含事务日志目录的磁盘受到良好保护。
清单 6: 执行备份,启用事务日志归档模式,并保存这次备份之前归档的事务日志
public void backupAndEnableLogArchiveMode(Connection conn) throws SQLException
{
String sqlstmt = "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE_AND_ENABLE_LOG_ARCHIVE_MODE(?, ?)";
CallableStatement cs = conn.prepareCall(sqlstmt);
cs.setString(1, "D:/dbbackups/");
cs.setInt(2, 0);
cs.execute();
cs.close();
}
|
清单 7: 执行备份,启用事务日志归档模式,并删除这次备份之前归档的事务日志
public void backupAndEnableLogArchiveMode(Connection conn) throws SQLException
{
String sqlstmt = "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE_AND_ENABLE_LOG_ARCHIVE_MODE(?, ?)";
CallableStatement cs = conn.prepareCall(sqlstmt);
cs.setString(1, "D:/dbbackups/");
cs.setInt(2, 1);
cs.execute();
cs.close();
}
|
使用前滚恢复方式恢复数据库
通过在到数据库的第一次连接的连接 URL 中指定属性 rollForwardRecoveryFrom=<backup path> ,可以使用前滚恢复来恢复数据库。注意,backup path 必须包括备份中的数据库名,而不是只包含备份位置。
数据库将被恢复到连接 URL 中指定的位置,或者,如果连接 URL 中没有指定位置的话,则恢复到当前 derby.system.home 位置。默认情况下,前滚恢复假设该位置就是数据库先前所在的位置。如果将数据库恢复到一个新的位置,可以使用 logDevice=<log dir> 属性指定事务日志的位置,这个位置应该与事务日志之前所在的位置一致。
清单 8: 使用前滚恢复从备份恢复 salesdb 数据库:
String dbURL = "jdbc:derby:salesdb;rollForwardRecoveryFrom=D:/dbbackups/salesdb";
Connection conn = DriverManager.getConnection(dbURL);
|
禁用事务日志归档模式
为了防备出现故障,可以启用事务日志归档模式来执行前滚恢复,这是一个很好的做法,但是,如果系统的事务日志存储空间被消耗光了,那么最好暂时禁用事务日志归档模式,这比让系统离线好。用于禁用事务日志归档模式的系统过程是
SYSCS_UTIL.SYSCS_DISABLE_LOG_ARCHIVE_MODE(IN SMALLINT DELETE_ARCHIVED_LOG_FILES )。
启用事务日志归档模式的一个副作用是:在下面两种情况下,所有数据更改(insert)都会被记录到日志中,而默认情况下它们不会被记录:
- 在包含数据的表上创建索引。
- 使用 Import 过程将外部数据导入到一个空表中。
在上述情况下,如果性能很重要的话,最好是在执行上述操作时禁用事务日志归档模式。
清单 9: 禁用事务日志归档模式,并删除所有归档的事务日志文件
public void disableLogArchiveMode(Connection conn) throws SQLException
{
String sqlstmt = "CALL SYSCS_UTIL.SYSCS_DISABLE_LOG_ARCHIVE_MODE(?)";
CallableStatement cs = conn.prepareCall(sqlstmt);
cs.setInt(1, 1);
cs.execute();
cs.close();
}
|
检查事务日志归档模式是否启用
通过检查数据库属性 derby.storage.logArchiveMode ,可以发现事务日志归档模式是否被启用。如果
事务日志归档模式被启用,那么这个属性将被设为 true。
清单 10: 查看事务日志归档模式状态
private boolean isLogArchiveModeOn(Connection conn) throws SQLException
{
Statement stmt = conn.createStatement();
String sqlstmt =
"VALUES SYSCS_UTIL.SYSCS_GET_DATABASE_PROPERTY('derby.storage.logArchiveMode')";
ResultSet rs = stmt.executeQuery(sqlstmt);
rs.next();
boolean logArchiveStatus = rs.getBoolean(1);
rs.close();
stmt.close();
return logArchiveStatus;
}
|
备份调度器
以特定方式进行备份适用于数据更改较少的应用程序。如果要更频繁地更改数据库,那么最好有某种特定机制来定期进行备份。如果应用程序已经在执行定时任务,那么可以将备份任务添加到现有列表中,否则应该实现某种机制,以便定期进行数据库备份。下面是一个示例代码,可以将它用到应用程序代码中,以便每天调用一次备份:
清单 11: BackupScheduler.java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Calendar;
import java.util.Date;
import java.text.SimpleDateFormat;
/*
* This class implements a backup scheduler to schedule database backups once a
* day at a specified time after a day it is started.
*
* Example usage in an application:
* BackupScheduler backups = new BackupScheduler("jdbc:derby:salesdb", "D:/backups");
*
* backups.start(23) //run database backups at 11PM everyday.
*
* @see java.util.Timer
* @see java.util.TimerTask
*/
public class BackupScheduler extends TimerTask
{
private final static long BACKUP_INTERVAL = 1000*60*60*24; //once a day
private String dbURL;
private String backupPath;
private Timer timer;
/* Constructor for Backup Scheduler
* @parm dbURL connection URL that should be used to connect to the database.
* @param backUpPath Location where the backup should be placed.
*/
public BackupScheduler(String dbURL, String backupPath) {
this.dbURL = dbURL;
this.backupPath = backupPath;
}
/**
* start the backup scheduler.
* @param backupTime the time of the date at which to start the backup
* in 24-hour clock notation.(eg: 1PM 13))
*/
public void start(int backupTime)
{
timer = new Timer();
//schedule a backup task everyday at the specified time, starting tomorrow.
timer.scheduleAtFixedRate(this,
getTomorrowTime(backupTime),
BACKUP_INTERVAL);
}
/**
* Implements TimerTask's run method to perform backups.
*/
public void run(){
try{
new org.apache.derby.jdbc.EmbeddedDriver();
Connection conn = DriverManager.getConnection(dbURL);
backupDatabase(conn);
conn.close();
}catch(Exception ex)
{
System.out.println("backup failed on:" + dbURL);
ex.printStackTrace();
}
}
/**
* Performs back up of the database
* @param conn Connnection to the database that is to be backed up.
*/
private void backupDatabase(Connection conn) throws SQLException
{
SimpleDateFormat dateFormat = new SimpleDateFormat("MM.dd.yy");
String backupDirectory = backupPath + "/" + dateFormat.format(new Date());
String sqlstmt = "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)";
CallableStatement cs = conn.prepareCall(sqlstmt);
cs.setString(1, backupDirectory);
cs.execute();
cs.close();
}
/**
* Gets the tomorrows Calenders time at the speficied hour.
* @param hourOfDay hour the time to be calculated.
* @return returns tomorrow time.
*/
private Date getTomorrowTime(int hourOfDay){
Calendar tomorrow = Calendar.getInstance();
tomorrow.roll(Calendar.DATE, true);
tomorrow.set(Calendar.HOUR_OF_DAY, hourOfDay);
tomorrow.set(Calendar.MINUTE, 0);
tomorrow.set(Calendar.SECOND , 0);
tomorrow.set(Calendar.MILLISECOND, 0);
return tomorrow.getTime();
}
}
|

 |

|
结束语
总之,IBM Cloudscape/Apache Derby 使得执行备份和恢复功能变得十分轻松。根据使用的资源和应用程序,通过下面的某一种配置设置系统可以使数据库在不同程度上免受磁盘故障的影响:
- 定期调度的在线/离线备份。通过使用这种配置,可以将数据库恢复到最后一次成功备份时的状态。
- 定期启用事务日志归档模式进行的在线备份,事务日志存储在一个容错磁盘上。通过使用这种配置,可以使用前滚恢复将数据库恢复到最近的状态。
- 硬件/软件容错技术,例如镜像,以保护存储数据库和事务日志信息的磁盘。在这种情况下,不需要备份/恢复,因为磁盘已经受到充分保护。
参考资料
关于作者  | |  | Suresh Thalamati 在 IBM Data Management 的 Cloudscape 小组担任软件工程师。从 1999 年开始,Suresh 一直从事 Cloudscape 开发领域的工作。 |
对本文的评价
|