qileilove

          blog已經(jīng)轉(zhuǎn)移至github,大家請訪問 http://qaseven.github.io/

          APDPlat中數(shù)據(jù)庫備份恢復(fù)的設(shè)計與實現(xiàn)

           APDPlat提供了web接口的數(shù)據(jù)庫備份與恢復(fù),支持手工操作和定時調(diào)度,可下載備份文件到本地,也可把備份文件發(fā)送到異地容錯,極大地簡化了數(shù)據(jù)庫的維護工作
            設(shè)計目標:
            1、多數(shù)據(jù)庫支持
            2、橫切關(guān)注點隔離
            3、異地容錯
            下面闡述具體的設(shè)計及實現(xiàn):
            1、為了支持多數(shù)據(jù)庫,統(tǒng)一的接口是不可避免的,如下所示:
          /**
          * 備份恢復(fù)數(shù)據(jù)庫接口
          * @author 楊尚川
          */
          public interface BackupService {
          /**
          * 備份數(shù)據(jù)庫
          * @return 是否備份成功
          */
          public boolean backup();
          /**
          * 恢復(fù)數(shù)據(jù)庫
          * @param date
          * @return 是否恢復(fù)成功
          */
          public boolean restore(String date);
          /**
          * 獲取已經(jīng)存在的備份文件名稱列表
          * @return  備份文件名稱列表
          */
          public List<String> getExistBackupFileNames();
          /**
          * 獲取備份文件存放的本地文件系統(tǒng)路徑
          * @return 備份文件存放路徑
          */
          public String getBackupFilePath();
          /**
          * 獲取最新的備份文件
          * @return 最新的備份文件
          */
          public File getNewestBackupFile();}


           對于各個不同的數(shù)據(jù)庫來說,有一些通用的操作,如對加密的數(shù)據(jù)庫用戶名和密碼的解密操作,還有接口定義的備份文件存放的本地文件系統(tǒng)路徑,用一個抽象類來實現(xiàn)接口中的通用方法以及其他通用方法如decrypt:
          /**
          *備份恢復(fù)數(shù)據(jù)庫抽象類,抽象出了針對各個數(shù)據(jù)庫來說通用的功能
          * @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;
          //從配置文件中獲取數(shù)據(jù)庫用戶名和密碼,如果用戶名和密碼被加密,則解密
          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數(shù)據(jù)庫的實現(xiàn):
          /**
          *MySQL備份恢復(fù)實現(xiàn)
          * @author 楊尚川
          */
          @Service("MYSQL")
          public class MySQLBackupService extends AbstractBackupService{
          /**
          * MySQL備份數(shù)據(jù)庫實現(xiàn)
          * @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恢復(fù)數(shù)據(jù)庫實現(xiàn)
          * @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+" 恢復(fù)");
          return true;
          } catch (Exception e) {
          LOG.error("恢復(fù)出錯",e);
          }
          return false;
          }
          }
            這里的關(guān)鍵有兩點,一是從配置文件db.properties或db.local.properties中獲取指定的命令進行備份和恢復(fù)操作,二是為實現(xiàn)類指定注解@Service("MYSQL"),這里服務(wù)名稱必須和配置文件db.properties或db.local.properties中jpa.database的值一致,jpa.database的值指定了當前使用哪一種數(shù)據(jù)庫,如下所示:
          #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}
           有了接口和多個實現(xiàn),那么備份和恢復(fù)的時候究竟選擇哪一種數(shù)據(jù)庫實現(xiàn)呢?BackupServiceExecuter充當工廠類(Factory),負責從多個數(shù)據(jù)庫備份恢復(fù)實現(xiàn)類中選擇一個并執(zhí)行相應(yīng)的備份和恢復(fù)操作,BackupServiceExecuter也實現(xiàn)了BackupService接口,這也是一個典型的外觀(Facade)設(shè)計模式,封裝了選擇特定數(shù)據(jù)庫的邏輯。
            定時調(diào)度器和web前端控制器也是使用BackupServiceExecuter來執(zhí)行備份恢復(fù)操作,BackupServiceExecuter通過每個實現(xiàn)類以@Service注解指定的名稱以及配置文件
          db.properties或db.local.properties中jpa.database的值來做選擇的依據(jù),如下所示:
          /**
          *執(zhí)行備份恢復(fù)的服務(wù),自動判斷使用的是什么數(shù)據(jù)庫,并找到該數(shù)據(jù)庫備份恢復(fù)服務(wù)的實現(xiàn)并執(zhí)行
          * @author 楊尚川
          */
          @Service
          public class BackupServiceExecuter extends AbstractBackupService{
          private BackupService backupService=null;
          @Resource(name="backupFileSenderExecuter")
          private BackupFileSenderExecuter backupFileSenderExecuter;
          /**
          * 查找并執(zhí)行正在使用的數(shù)據(jù)的備份實現(xiàn)實例
          * @return
          */
          @Override
          public boolean backup() {
          if(backupService==null){
          backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
          }
          boolean result = backupService.backup();
          //如果備份成功,則將備份文件發(fā)往他處
          if(result){
          backupFileSenderExecuter.send(getNewestBackupFile());
          }
          return result;
          }
          /**
          * 查找并執(zhí)行正在使用的數(shù)據(jù)的恢復(fù)實現(xiàn)實例
          * @param date
          * @return
          */
          @Override
          public boolean restore(String date) {
          if(backupService==null){
          backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
          }
          return backupService.restore(date);
          }
          }
            關(guān)鍵是這行代碼backupService=SpringContextUtils.getBean(PropertyHolder.getProperty("jpa.database"));
            2、在記錄備份恢復(fù)日志的時候,如果每種數(shù)據(jù)庫的實現(xiàn)類都要粘貼復(fù)制通用的代碼到備份和恢復(fù)方法的開始和結(jié)束位置,那么四處就飄散著重復(fù)的代碼,對易讀性和可修改性都是極大的破壞。
            AOP是解決這個問題的不二之選,為了AOP能工作,良好設(shè)計的包結(jié)構(gòu)、類層級,規(guī)范的命名都是非常重要的,尤其是這里的BackupServiceExecuter和真正執(zhí)行備份恢復(fù)的實現(xiàn)類有共同的方法簽名(都實現(xiàn)了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 />
            最后就可以編寫代碼實現(xiàn)日志記錄:
          /**
          * 備份恢復(fù)數(shù)據(jù)庫日志Aspect
          * org.apdplat.module.system.service.backup.impl包下面有多個數(shù)據(jù)庫的備份恢復(fù)實現(xiàn)
          * 他們實現(xiàn)了BackupService接口的backup方法(備份數(shù)據(jù)庫)和restore(恢復(fù)數(shù)據(jù)庫)方法
          * @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("啟用備份恢復(fù)日志");
          LOG.info("Enable backup restore log", Locale.ENGLISH);
          }else{
          LOG.info("禁用備份恢復(fù)日志");
          LOG.info("Disable backup restore log", Locale.ENGLISH);
          }
          }
          //攔截備份數(shù)據(jù)庫操作
          @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);
          }
          }
          //攔截恢復(fù)數(shù)據(jù)庫操作
          @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("準備記錄數(shù)據(jù)庫"+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("無法獲取服務(wù)器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());
          //將日志加入內(nèi)存緩沖區(qū)
          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、怎么樣才能異地容錯呢?將備份文件保存到與服務(wù)器處于不同地理位置的機器上,最好能多保存幾份。除了能自動把備份文件傳輸?shù)疆惖胤?wù)器上面,用戶也可以從web界面下載。
            APDPlat使用推模型來發(fā)送備份文件,接口如下:
          /**
          * 備份文件發(fā)送器
          * 將最新的備份文件發(fā)送到其他機器,防止服務(wù)器故障丟失數(shù)據(jù)
          * @author 楊尚川
          */
          public interface BackupFileSender {
          public void send(File file);
          }

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


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


          網(wǎng)站導(dǎo)航:
           
          <2014年2月>
          2627282930311
          2345678
          9101112131415
          16171819202122
          2324252627281
          2345678

          導(dǎo)航

          統(tǒng)計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 宁德市| 建平县| 舒城县| 二连浩特市| 定南县| 唐海县| 百色市| 神木县| 汪清县| 商城县| 武川县| 枣强县| 会昌县| 长汀县| 许昌县| 浦城县| 克山县| 图们市| 安徽省| 肃宁县| 哈巴河县| 依兰县| 资阳市| 信阳市| 尉犁县| 临泽县| 远安县| 安图县| 荣昌县| 武川县| 鲁山县| 澳门| 根河市| 闸北区| 奉节县| 乐东| 泗洪县| 桂阳县| 安岳县| 龙陵县| 榕江县|