qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          APDPlat中數據庫備份恢復的設計與實現

           APDPlat提供了web接口的數據庫備份與恢復,支持手工操作和定時調度,可下載備份文件到本地,也可把備份文件發送到異地容錯,極大地簡化了數據庫的維護工作
            設計目標:
            1、多數據庫支持
            2、橫切關注點隔離
            3、異地容錯
            下面闡述具體的設計及實現:
            1、為了支持多數據庫,統一的接口是不可避免的,如下所示:
          /**
          * 備份恢復數據庫接口
          * @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:
          /**
          *備份恢復數據庫抽象類,抽象出了針對各個數據庫來說通用的功能
          * @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數據庫的實現:
          /**
          *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的值指定了當前使用哪一種數據庫,如下所示:
          #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的值來做選擇的依據,如下所示:
          /**
          *執行備份恢復的服務,自動判斷使用的是什么數據庫,并找到該數據庫備份恢復服務的實現并執行
          * @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首先要引入依賴:
          <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配置文件中指定啟用自動代理:
            <aop:aspectj-autoproxy />
            最后就可以編寫代碼實現日志記錄:
          /**
          * 備份恢復數據庫日志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("記錄完畢");
          }
          }
          select
              Vcs_Accd_Info.Date_Rcv AS "Date Rcv",
              Vcs_Accd_Info.Date_Send AS "Date Send",
              Vcs_Accd_Info.Rgn_Accd_Code AS "Rgn Accd Code",
              Count (*) AS "Case Cnt",
              Vcd_Sys_Cd.Desc AS "Region",
              'THIS' AS "Period",
              Vmi_Report_Date.Date_Rpt_Start AS "Date Rpt Start",
              Vmi_Report_Date.Date_Rpt_End AS "Date Rpt End",
              to_char(Vmi_Report_Date.Date_Rpt_Start ,'DD-MM-YYYY') ||' - '||to_char(Vmi_Report_Date.Date_Rpt_End,'DD-MM-YYYY') AS "Rep",
              case when Count (*)= 0 then 0 else days(Vcs_Accd_Info.Date_Rcv) - days(Vcs_Accd_Info.Date_Send)+1 end "Duration",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '1' then Count (*)  else 0 end "THKI_Case_Cnt",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '2' then Count (*)  else 0 end "TKE_Case_Cnt",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '3' then Count (*)  else 0 end "TKW_Case_Cnt",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '5' then Count (*)  else 0 end "TNTN_Case_Cnt",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '4' then Count (*)  else 0 end "TNTS_Case_Cnt",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '6' then Count (*)  else 0 end "OTH_Case_Cnt",
                case when Vcs_Accd_Info.Rgn_Accd_Code  = '' or Vcs_Accd_Info.Rgn_Accd_Code is null then 1 else 0 end "Unknown_Case_Cnt"
              FROM
              Vcs_Accd_Info, 
              Vcd_Sys_Cd,Vmi_Report_Date 
              WHERE
              (Vcs_Accd_Info.Last_Upd_Ts<>Vmi_Report_Date.Join_Ts
              AND Vcd_Sys_Cd.Last_Upd_Ts<>Vcs_Accd_Info.Last_Upd_Ts)
              AND ((Vcd_Sys_Cd.Sys_Code_Type='RG1'
              AND Vcs_Accd_Info.Date_Rcv >= Vmi_Report_Date.Date_Rpt_Start
              and Vcs_Accd_Info.Date_Rcv <= Vmi_Report_Date.Date_Rpt_End 
              AND Vmi_Report_Date.Rpt_Group=6
              AND Vcd_Sys_Cd.sys_code_key = Vcs_Accd_Info.Rgn_Accd_Code
              AND Vcs_Accd_Info.Date_Send<>'1111-11-11'))
                
              GROUP BY
              Vcs_Accd_Info.DATE_RCV, 
              Vcs_Accd_Info.DATE_SEND, 
              Vcs_Accd_Info.Rgn_Accd_Code, 
              Vcd_Sys_Cd.DESC, 
              'THIS', 
              Vmi_Report_Date.DATE_RPT_START, 
              Vmi_Report_Date.DATE_RPT_END,
              to_char(Vmi_Report_Date.Date_Rpt_Start ,'DD-MM-YYYY') ||' - '||to_char(Vmi_Report_Date.Date_Rpt_End,'DD-MM-YYYY')
            3、怎么樣才能異地容錯呢?將備份文件保存到與服務器處于不同地理位置的機器上,最好能多保存幾份。除了能自動把備份文件傳輸到異地服務器上面,用戶也可以從web界面下載。
            APDPlat使用推模型來發送備份文件,接口如下:
          /**
          * 備份文件發送器
          * 將最新的備份文件發送到其他機器,防止服務器故障丟失數據
          * @author 楊尚川
          */
          public interface BackupFileSender {
          public void send(File file);
          }

          posted on 2014-02-12 12:55 順其自然EVO 閱讀(368) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          <2014年2月>
          2627282930311
          2345678
          9101112131415
          16171819202122
          2324252627281
          2345678

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 习水县| 当雄县| 丹巴县| 城市| 龙川县| 青浦区| 全南县| 弋阳县| 丁青县| 大竹县| 来安县| 集贤县| 西乌| 高雄县| 翁源县| 辽宁省| 巴林左旗| 定日县| 沙洋县| 石林| 建始县| 敦化市| 温泉县| 金山区| 邛崃市| 潞城市| 慈利县| 石柱| 清河县| 七台河市| 来宾市| 封开县| 龙里县| 江安县| 梅州市| 明光市| 张家口市| 富川| 凯里市| 大竹县| 巢湖市|