技術實現(2)之資料庫備份恢復的設計與實現
APDPlat提供了web介面的資料庫備份與恢復,支援手工操作和定時排程,可下載備份檔案到本地,也可把備份檔案傳送到異地容錯,極大地簡化了資料庫的維護工作。
設計目標:
1、多資料庫支援
2、橫切關注點隔離
3、異地容錯
下面闡述具體的設計及實現:
1、為了支援多資料庫,統一的介面是不可避免的,如下所示:
Java程式碼- /**
- * 備份恢復資料庫介面
- * @author 楊尚川
- */
- publicinterface BackupService {
- /**
- * 備份資料庫
- * @return 是否備份成功
- */
- publicboolean
- /**
- * 恢復資料庫
- * @param date
- * @return 是否恢復成功
- */
- publicboolean restore(String date);
- /**
- * 獲取已經存在的備份檔名稱列表
- * @return 備份檔名稱列表
- */
- public List<String> getExistBackupFileNames();
- /**
- * 獲取備份檔案存放的本地檔案系統路徑
- * @return 備份檔案存放路徑
- */
- public String getBackupFilePath();
- /**
- * 獲取最新的備份檔案
- * @return 最新的備份檔案
- */
- public File getNewestBackupFile();}
/** * 備份恢復資料庫介面 * @author 楊尚川 */ public interface BackupService { /** * 備份資料庫 * @return 是否備份成功 */ public boolean backup(); /** * 恢復資料庫 * @param date * @return 是否恢復成功 */ public boolean restore(String date); /** * 獲取已經存在的備份檔名稱列表 * @return 備份檔名稱列表 */ public List<String> getExistBackupFileNames(); /** * 獲取備份檔案存放的本地檔案系統路徑 * @return 備份檔案存放路徑 */ public String getBackupFilePath(); /** * 獲取最新的備份檔案 * @return 最新的備份檔案 */ public File getNewestBackupFile();}
對於各個不同的資料庫來說,有一些通用的操作,如對加密的資料庫使用者名稱和密碼的解密操作,還有介面定義的備份檔案存放的本地檔案系統路徑,用一個抽象類來實現介面中的通用方法以及其他通用方法如decrypt:
Java程式碼- /**
- *備份恢復資料庫抽象類,抽象出了針對各個資料庫來說通用的功能
- * @author 楊尚川
- */
- publicabstractclass AbstractBackupService implements BackupService{
- protectedfinal APDPlatLogger LOG = new APDPlatLogger(getClass());
- protectedstaticfinal StandardPBEStringEncryptor encryptor;
- protectedstaticfinal String username;
- protectedstaticfinal String password;
- //從配置檔案中獲取資料庫使用者名稱和密碼,如果使用者名稱和密碼被加密,則解密
- static{
- EnvironmentStringPBEConfig config=new EnvironmentStringPBEConfig();
- config.setAlgorithm("PBEWithMD5AndDES");
- config.setPassword("config");
- encryptor=new StandardPBEStringEncryptor();
- encryptor.setConfig(config);
- String uname=PropertyHolder.getProperty("db.username");
- String pwd=PropertyHolder.getProperty("db.password");
- if(uname!=null && uname.contains("ENC(") && uname.contains(")")){
- uname=uname.substring(4,uname.length()-1);
- username=decrypt(uname);
- }else{
- username=uname;
- }
- if(pwd!=null && pwd.contains("ENC(") && pwd.contains(")")){
- pwd=pwd.substring(4,pwd.length()-1);
- password=decrypt(pwd);
- }else{
- password=pwd;
- }
- }
- @Override
- public String getBackupFilePath(){
- String path="/WEB-INF/backup/"+PropertyHolder.getProperty("jpa.database")+"/";
- path=FileUtils.getAbsolutePath(path);
- File file=new File(path);
- if(!file.exists()){
- file.mkdirs();
- }
- return path;
- }
- @Override
- public File getNewestBackupFile(){
- Map<String,File> map = new HashMap<>();
- List<String> list = new ArrayList<>();
- String path=getBackupFilePath();
- File dir=new File(path);
- File[] files=dir.listFiles();
- for(File file : files){
- String name=file.getName();
- if(!name.contains("bak")) {
- continue;
- }
- map.put(name, file);
- list.add(name);
- }
- if(list.isEmpty()){
- returnnull;
- }
- //按備份時間排序
- Collections.sort(list);
- //最新備份的在最前面
- Collections.reverse(list);
- String name = list.get(0);
- File file = map.get(name);
- //加速垃圾回收
- list.clear();
- map.clear();
- return file;
- } @Override
- public List<String> getExistBackupFileNames(){
- List<String> result=new ArrayList<>();
- String path=getBackupFilePath();
- File dir=new File(path);
- File[] files=dir.listFiles();
- for(File file : files){
- String name=file.getName();
- if(!name.contains("bak")) {
- continue;
- }
- name=name.substring(0, name.length()-4);
- String[] temp=name.split("-");
- String y=temp[0];
- String m=temp[1];
- String d=temp[2];
- String h=temp[3];
- String mm=temp[4];
- String s=temp[5];
- name=y+"-"+m+"-"+d+" "+h+":"+mm+":"+s;
- result.add(name);
- }
- //按備份時間排序
- Collections.sort(result);
- //最新備份的在最前面
- Collections.reverse(result);
- return result;
- }
- /**
- * 解密使用者名稱和密碼
- * @param encryptedMessage 加密後的使用者名稱或密碼
- * @return 解密後的使用者名稱或密碼
- */
- protectedstatic String decrypt(String encryptedMessage){
- String plain=encryptor.decrypt(encryptedMessage);
- return plain;
- }
- }
/**
*備份恢復資料庫抽象類,抽象出了針對各個資料庫來說通用的功能
* @author 楊尚川
*/
public abstract class AbstractBackupService implements BackupService{
protected final APDPlatLogger LOG = new APDPlatLogger(getClass());
protected static final StandardPBEStringEncryptor encryptor;
protected static final String username;
protected static final String password;
//從配置檔案中獲取資料庫使用者名稱和密碼,如果使用者名稱和密碼被加密,則解密
static{
EnvironmentStringPBEConfig config=new EnvironmentStringPBEConfig();
config.setAlgorithm("PBEWithMD5AndDES");
config.setPassword("config");
encryptor=new StandardPBEStringEncryptor();
encryptor.setConfig(config);
String uname=PropertyHolder.getProperty("db.username");
String pwd=PropertyHolder.getProperty("db.password");
if(uname!=null && uname.contains("ENC(") && uname.contains(")")){
uname=uname.substring(4,uname.length()-1);
username=decrypt(uname);
}else{
username=uname;
}
if(pwd!=null && pwd.contains("ENC(") && pwd.contains(")")){
pwd=pwd.substring(4,pwd.length()-1);
password=decrypt(pwd);
}else{
password=pwd;
}
}
@Override
public String getBackupFilePath(){
String path="/WEB-INF/backup/"+PropertyHolder.getProperty("jpa.database")+"/";
path=FileUtils.getAbsolutePath(path);
File file=new File(path);
if(!file.exists()){
file.mkdirs();
}
return path;
}
@Override
public File getNewestBackupFile(){
Map<String,File> map = new HashMap<>();
List<String> list = new ArrayList<>();
String path=getBackupFilePath();
File dir=new File(path);
File[] files=dir.listFiles();
for(File file : files){
String name=file.getName();
if(!name.contains("bak")) {
continue;
}
map.put(name, file);
list.add(name);
}
if(list.isEmpty()){
return null;
}
//按備份時間排序
Collections.sort(list);
//最新備份的在最前面
Collections.reverse(list);
String name = list.get(0);
File file = map.get(name);
//加速垃圾回收
list.clear();
map.clear();
return file;
} @Override
public List<String> getExistBackupFileNames(){
List<String> result=new ArrayList<>();
String path=getBackupFilePath();
File dir=new File(path);
File[] files=dir.listFiles();
for(File file : files){
String name=file.getName();
if(!name.contains("bak")) {
continue;
}
name=name.substring(0, name.length()-4);
String[] temp=name.split("-");
String y=temp[0];
String m=temp[1];
String d=temp[2];
String h=temp[3];
String mm=temp[4];
String s=temp[5];
name=y+"-"+m+"-"+d+" "+h+":"+mm+":"+s;
result.add(name);
}
//按備份時間排序
Collections.sort(result);
//最新備份的在最前面
Collections.reverse(result);
return result;
}
/**
* 解密使用者名稱和密碼
* @param encryptedMessage 加密後的使用者名稱或密碼
* @return 解密後的使用者名稱或密碼
*/
protected static String decrypt(String encryptedMessage){
String plain=encryptor.decrypt(encryptedMessage);
return plain;
}
}
下面來看一個MySQL資料庫的實現:
Java程式碼- /**
- *MySQL備份恢復實現
- * @author 楊尚川
- */
- @Service("MYSQL")
- publicclass MySQLBackupService extends AbstractBackupService{
- /**
- * MySQL備份資料庫實現
- * @return
- */
- @Override
- publicboolean backup() {
- try {
- String path=getBackupFilePath()+DateTypeConverter.toFileName(new Date())+".bak";
- String command=PropertyHolder.getProperty("db.backup.command");
- command=command.replace("${db.username}", username);
- command=command.replace("${db.password}", password);
- command=command.replace("${module.short.name}", PropertyHolder.getProperty("module.short.name"));
- Runtime runtime = Runtime.getRuntime();
- Process child = runtime.exec(command);
- InputStream in = child.getInputStream();
- try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), "utf8");BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf8"))){
- String line=reader.readLine();
- while (line != null) {
- writer.write(line+"\n");
- line=reader.readLine();
- }
- writer.flush();
- }
- LOG.debug("備份到:"+path);
- returntrue;
- } catch (Exception e) {
- LOG.error("備份出錯",e);
- }
- returnfalse;
- }
- /**
- * MySQL恢復資料庫實現
- * @param date
- * @return
- */
- @Override
- publicboolean restore(String date) {
- try {
- String path=getBackupFilePath()+date+".bak";
- String command=PropertyHolder.getProperty("db.restore.command");
- command=command.replace("${db.username}", username);
- command=command.replace("${db.password}", password);
- command=command.replace("${module.short.name}", PropertyHolder.getProperty("module.short.name"));
- Runtime runtime = Runtime.getRuntime();
- Process child = runtime.exec(command);
- try(OutputStreamWriter writer = new OutputStreamWriter(child.getOutputStream(), "utf8");BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "utf8"))){
- String line=reader.readLine();
- while (line != null) {
- writer.write(line+"\n");
- line=reader.readLine();
- }
- writer.flush();
- }
- LOG.debug("從 "+path+" 恢復");
- returntrue;
- } catch (Exception e) {
- LOG.error("恢復出錯",e);
- }
- returnfalse;
- }
- }
/**
*MySQL備份恢復實現
* @author 楊尚川
*/
@Service("MYSQL")
public class MySQLBackupService extends AbstractBackupService{
/**
* MySQL備份資料庫實現
* @return
*/
@Override
public boolean backup() {
try {
String path=getBackupFilePath()+DateTypeConverter.toFileName(new Date())+".bak";
String command=PropertyHolder.getProperty("db.backup.command");
command=command.replace("${db.username}", username);
command=command.replace("${db.password}", password);
command=command.replace("${module.short.name}", PropertyHolder.getProperty("module.short.name"));
Runtime runtime = Runtime.getRuntime();
Process child = runtime.exec(command);
InputStream in = child.getInputStream();
try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), "utf8");BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf8"))){
String line=reader.readLine();
while (line != null) {
writer.write(line+"\n");
line=reader.readLine();
}
writer.flush();
}
LOG.debug("備份到:"+path);
return true;
} catch (Exception e) {
LOG.error("備份出錯",e);
}
return false;
}
/**
* MySQL恢復資料庫實現
* @param date
* @return
*/
@Override
public boolean restore(String date) {
try {
String path=getBackupFilePath()+date+".bak";
String command=PropertyHolder.getProperty("db.restore.command");
command=command.replace("${db.username}", username);
command=command.replace("${db.password}", password);
command=command.replace("${module.short.name}", PropertyHolder.getProperty("module.short.name"));
Runtime runtime = Runtime.getRuntime();
Process child = runtime.exec(command);
try(OutputStreamWriter writer = new OutputStreamWriter(child.getOutputStream(), "utf8");BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "utf8"))){
String line=reader.readLine();
while (line != null) {
writer.write(line+"\n");
line=reader.readLine();
}
writer.flush();
}
LOG.debug("從 "+path+" 恢復");
return true;
} catch (Exception e) {
LOG.error("恢復出錯",e);
}
return false;
}
}
這裡的關鍵有兩點,一是從配置檔案db.properties或db.local.properties中獲取指定的命令進行備份和恢復操作,二是為實現類指定註解@Service("MYSQL"),這裡服務名稱必須和配置檔案db.properties或db.local.properties中jpa.database的值一致,jpa.database的值指定了當前使用哪一種資料庫,如下所示:
Java程式碼- #mysql
- db.driver=com.mysql.jdbc.Driver
- db.url=jdbc:mysql://localhost:3306/${module.short.name}?useUnicode=true&characterEncoding=UTF-8&createDatabaseIfNotExist=true&autoReconnect=true
- db.username=ENC(i/TOu44AD6Zmz0fJwC32jQ==)
- db.password=ENC(i/TOu44AD6Zmz0fJwC32jQ==)
- jpa.database=MYSQL
- db.backup.command=mysqldump -u${db.username} -p${db.password} ${module.short.name}
- db.restore.command=mysql -u${db.username} -p${db.password} ${module.short.name}
#mysql
db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/${module.short.name}?useUnicode=true&characterEncoding=UTF-8&createDatabaseIfNotExist=true&autoReconnect=true
db.username=ENC(i/TOu44AD6Zmz0fJwC32jQ==)
db.password=ENC(i/TOu44AD6Zmz0fJwC32jQ==)
jpa.database=MYSQL
db.backup.command=mysqldump -u${db.username} -p${db.password} ${module.short.name}
db.restore.command=mysql -u${db.username} -p${db.password} ${module.short.name}
有了介面和多個實現,那麼備份和恢復的時候究竟選擇哪一種資料庫實現呢?BackupServiceExecuter充當工廠類(Factory),負責從多個數據庫備份恢復實現類中選擇一個並執行相應的備份和恢復操作,BackupServiceExecuter也實現了BackupService介面,這也是一個典型的外觀(Facade)設計模式,封裝了選擇特定資料庫的邏輯。
定時排程器和web前端控制器也是使用BackupServiceExecuter來執行備份恢復操作,BackupServiceExecuter通過每個實現類以@Service註解指定的名稱以及配置檔案db.properties或db.local.properties中jpa.database的值來做選擇的依據,如下所示:
Java程式碼- /**
- *執行備份恢復的服務,自動判斷使用的是什麼資料庫,並找到該資料庫備份恢復服務的實現並執行
- * @author 楊尚川
- */
- @Service
- publicclass BackupServiceExecuter extends AbstractBackupService{
- private BackupService backupService=null;
- @Resource(name="backupFileSenderExecuter")
- private BackupFileSenderExecuter backupFileSenderExecuter;
- /**
- * 查詢並執行正在使用的資料的備份實現例項
- * @return
- */
- @Override
- publicboolean backup() {
- if(backupService==null){
- backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
- }
- boolean result = backupService.backup();
- //如果備份成功,則將備份檔案發往他處
- if(result){
- backupFileSenderExecuter.send(getNewestBackupFile());
- }
- return result;
- }
- /**
- * 查詢並執行正在使用的資料的恢復實現例項
- * @param date
- * @return
- */
- @Override
- publicboolean restore(String date) {
- if(backupService==null){
- backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
- }
- return backupService.restore(date);
- }
- }
/**
*執行備份恢復的服務,自動判斷使用的是什麼資料庫,並找到該資料庫備份恢復服務的實現並執行
* @author 楊尚川
*/
@Service
public class BackupServiceExecuter extends AbstractBackupService{
private BackupService backupService=null;
@Resource(name="backupFileSenderExecuter")
private BackupFileSenderExecuter backupFileSenderExecuter;
/**
* 查詢並執行正在使用的資料的備份實現例項
* @return
*/
@Override
public boolean backup() {
if(backupService==null){
backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
}
boolean result = backupService.backup();
//如果備份成功,則將備份檔案發往他處
if(result){
backupFileSenderExecuter.send(getNewestBackupFile());
}
return result;
}
/**
* 查詢並執行正在使用的資料的恢復實現例項
* @param date
* @return
*/
@Override
public boolean restore(String date) {
if(backupService==null){
backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
}
return backupService.restore(date);
}
}
關鍵是這行程式碼backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
2、在記錄備份恢復日誌的時候,如果每種資料庫的實現類都要貼上複製通用的程式碼到備份和恢復方法的開始和結束位置,那麼四處就飄散著重複的程式碼,對易讀性和可修改性都是極大的破壞。
AOP是解決這個問題的不二之選,為了AOP能工作,良好設計的包結構、類層級,規範的命名都是非常重要的,尤其是這裡的BackupServiceExecuter和真正執行備份恢復的實現類有共同的方法簽名(都實現了BackupService介面),所以把他們放到不同的包裡有利於AOP。
使用AOP首先要引入依賴:
Xml程式碼- <dependency>
- <groupId>org.aspectj</groupId>
- <artifactId>aspectjrt</artifactId>
- <version>${aspectj.version}</version>
- </dependency>
- <dependency>
- <groupId>org.aspectj</groupId>
- <artifactId>aspectjweaver</artifactId>
- <version>${aspectj.version}</version>
- </dependency>
其次是要在spring配置檔案中指定啟用自動代理:
Xml程式碼- <aop:aspectj-autoproxy/>
最後就可以編寫程式碼實現日誌記錄:
Java程式碼- /**
- * 備份恢復資料庫日誌Aspect
- * org.apdplat.module.system.service.backup.impl包下面有多個數據庫的備份恢復實現
- * 他們實現了BackupService介面的backup方法(備份資料庫)和restore(恢復資料庫)方法
- * @author 楊尚川
- */
- @Aspect
- @Service
- publicclass BackupLogAspect {
- privatestaticfinal APDPlatLogger LOG = new APDPlatLogger(BackupLogAspect.class);
- privatestaticfinalboolean MONITOR_BACKUP = PropertyHolder.getBooleanProperty("monitor.backup");
- private BackupLog backupLog = null;
- static{
- if(MONITOR_BACKUP){
- LOG.info("啟用備份恢復日誌");
- LOG.info("Enable backup restore log", Locale.ENGLISH);
- }else{
- LOG.info("禁用備份恢復日誌");
- LOG.info("Disable backup restore log", Locale.ENGLISH);
- }
- }
- //攔截備份資料庫操作
- @Pointcut("execution( boolean org.apdplat.module.system.service.backup.impl.*.backup() )")
- publicvoid backup() {}
- @Before("backup()")
- publicvoid beforeBackup(JoinPoint jp) {
- if(MONITOR_BACKUP){
- before(BackupLogType.BACKUP);
- }
- }
- @AfterReturning(value="backup()", argNames="result", returning = "result")
- publicvoid afterBackup(JoinPoint jp, boolean result) {
- if(MONITOR_BACKUP){
- after(result);
- }
- }
- //攔截恢復資料庫操作
- @Before(value="execution( boolean org.apdplat.module.system.service.backup.impl.*.restore(java.lang.String) ) && args(date)",
- argNames="date")
- publicvoid beforeRestore(JoinPoint jp, String date) {
- if(MONITOR_BACKUP){
- before(BackupLogType.RESTORE);
- }
- }
- @AfterReturning(pointcut="execution( boolean org.apdplat.module.system.service.backup.impl.*.restore(java.lang.String) )",
- returning = "result")
- publicvoid afterRestore(JoinPoint jp, boolean result) {
- if(MONITOR_BACKUP){
- after(result);
- }
- }
- privatevoid before(String type){
- LOG.info("準備記錄資料庫"+type+"日誌");
- User user=UserHolder.getCurrentLoginUser();
- String ip=UserHolder.getCurrentUserLoginIp();
- backupLog=new BackupLog();
- if(user != null){
- backupLog.setUsername(user.getUsername());
- }
- backupLog.setLoginIP(ip);
- try {
- backupLog.setServerIP(InetAddress.getLocalHost().getHostAddress());
- } catch (UnknownHostException e) {
- LOG.error("無法獲取伺服器IP地址", e);
- LOG.error("Can't get server's ip address", e, Locale.ENGLISH);
- }
- backupLog.setAppName(SystemListener.getContextPath());
- backupLog.setStartTime(new Date());
- backupLog.setOperatingType(type);
- }
- privatevoid after(boolean result){
- if(result){
- backupLog.setOperatingResult(BackupLogResult.SUCCESS);
- }else{
- backupLog.setOperatingResult(BackupLogResult.FAIL);
- }
- backupLog.setEndTime(new Date());
- backupLog.setProcessTime(backupLog.getEndTime().getTime()-backupLog.getStartTime().getTime());
- //將日誌加入記憶體緩衝區
- BufferLogCollector.collect(backupLog);
- LOG.info("記錄完畢");
- }
- }
/**
* 備份恢復資料庫日誌Aspect
* org.apdplat.module.system.service.backup.impl包下面有多個數據庫的備份恢復實現
* 他們實現了BackupService介面的backup方法(備份資料庫)和restore(恢復資料庫)方法
* @author 楊尚川
*/
@Aspect
@Service
public class BackupLogAspect {
private static final APDPlatLogger LOG = new APDPlatLogger(BackupLogAspect.class);
private static final boolean MONITOR_BACKUP = PropertyHolder.getBooleanProperty("monitor.backup");
private BackupLog backupLog = null;
static{
if(MONITOR_BACKUP){
LOG.info("啟用備份恢復日誌");
LOG.info("Enable backup restore log", Locale.ENGLISH);
}else{
LOG.info("禁用備份恢復日誌");
LOG.info("Disable backup restore log", Locale.ENGLISH);
}
}
//攔截備份資料庫操作
@Pointcut("execution( boolean org.apdplat.module.system.service.backup.impl.*.backup() )")
public void backup() {}
@Before("backup()")
public void beforeBackup(JoinPoint jp) {
if(MONITOR_BACKUP){
before(BackupLogType.BACKUP);
}
}
@AfterReturning(value="backup()", argNames="result", returning = "result")
public void afterBackup(JoinPoint jp, boolean result) {
if(MONITOR_BACKUP){
after(result);
}
}
//攔截恢復資料庫操作
@Before(value="execution( boolean org.apdplat.module.system.service.backup.impl.*.restore(java.lang.String) ) && args(date)",
argNames="date")
public void beforeRestore(JoinPoint jp, String date) {
if(MONITOR_BACKUP){
before(BackupLogType.RESTORE);
}
}
@AfterReturning(pointcut="execution( boolean org.apdplat.module.system.service.backup.impl.*.restore(java.lang.String) )",
returning = "result")
public void afterRestore(JoinPoint jp, boolean result) {
if(MONITOR_BACKUP){
after(result);
}
}
private void before(String type){
LOG.info("準備記錄資料庫"+type+"日誌");
User user=UserHolder.getCurrentLoginUser();
String ip=UserHolder.getCurrentUserLoginIp();
backupLog=new BackupLog();
if(user != null){
backupLog.setUsername(user.getUsername());
}
backupLog.setLoginIP(ip);
try {
backupLog.setServerIP(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException e) {
LOG.error("無法獲取伺服器IP地址", e);
LOG.error("Can't get server's ip address", e, Locale.ENGLISH);
}
backupLog.setAppName(SystemListener.getContextPath());
backupLog.setStartTime(new Date());
backupLog.setOperatingType(type);
}
private void after(boolean result){
if(result){
backupLog.setOperatingResult(BackupLogResult.SUCCESS);
}else{
backupLog.setOperatingResult(BackupLogResult.FAIL);
}
backupLog.setEndTime(new Date());
backupLog.setProcessTime(backupLog.getEndTime().getTime()-backupLog.getStartTime().getTime());
//將日誌加入記憶體緩衝區
BufferLogCollector.collect(backupLog);
LOG.info("記錄完畢");
}
}
3、怎麼樣才能異地容錯呢?將備份檔案儲存到與伺服器處於不同地理位置的機器上,最好能多儲存幾份。除了能自動把備份檔案傳輸到異地伺服器上面,使用者也可以從web介面下載。
APDPlat使用推模型來發送備份檔案,介面如下: