文档章节

APDPlat中数据库备份恢复的设计与实现

杨尚川
 杨尚川
发布于 2014/01/30 07:29
字数 2625
阅读 245
收藏 6

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("记录完毕");
    }
}

 

 

    3、怎么样才能异地容错呢?将备份文件保存到与服务器处于不同地理位置的机器上,最好能多保存几份。除了能自动把备份文件传输到异地服务器上面,用户也可以从web界面下载。

 

    APDPlat使用推模型来发送备份文件,接口如下:

 

/**
 * 备份文件发送器
 * 将最新的备份文件发送到其他机器,防止服务器故障丢失数据
 * @author 杨尚川
 */
public interface BackupFileSender {
    public void send(File file);
}

 

 

    有了统一的接口,就可以有灵活的实现方式,如通过HTTP、FTP、SOCKET等方式发送到异地机房。

 

    在上面的BackupServiceExecuter类中我们已经看到,当备份成功之后就会调用BackupFileSenderExecutersend方法发送备份文件,如下:

 

boolean result = backupService.backup();
//如果备份成功,则将备份文件发往他处
if(result){
	backupFileSenderExecuter.send(getNewestBackupFile());
}

 

 

    BackupFileSenderExecuter的设计和BackupServiceExecuter类似,不过策略不一样,如果配置有多个Sender,那么会调用所有的Sender,达到拥有多个副本的目的。BackupFileSenderExecuter利用配置项log.backup.file.sender的值来指定启用哪些Sender,并依次调用各个Sender的send方法来完成文件的发送,如下所示:

 

log.backup.file.sender=localBackupFileSender;

 

 

    这里 localBackupFileSenderLocalBackupFileSender的Spring Bean名称。

 

/**
 *执行备份文件的发送服务,根据配置文件来判断使用哪些发送器,并按配置的前后顺序依次调用
 * @author 杨尚川
 */
@Service
public class BackupFileSenderExecuter  implements  BackupFileSender, ApplicationListener{
    protected final APDPlatLogger LOG = new APDPlatLogger(getClass());
    private static final List<BackupFileSender> backupFileSenders = new LinkedList<>();  
    @Override
    public void send(File file) {
        for(BackupFileSender sender : backupFileSenders){
            sender.send(file);
        }
    }
    @Override
    public void onApplicationEvent(ApplicationEvent event){
            if(event instanceof ContextRefreshedEvent){
                    LOG.info("spring容器初始化完成,开始解析BackupFileSender");
                    String senderstr = PropertyHolder.getProperty("log.backup.file.sender");
                    if(StringUtils.isBlank(senderstr)){
                            LOG.info("未配置log.backup.file.sender");
                            return;
                    }
                    LOG.info("log.backup.file.sender:"+senderstr);
                    String[] senders = senderstr.trim().split(";");
                    for(String sender : senders){
                            BackupFileSender backupFileSender = SpringContextUtils.getBean(sender.trim());
                            if(backupFileSender != null){
                                    backupFileSenders.add(backupFileSender);
                                    LOG.info("找到BackupFileSender:"+sender);
                            }else{
                                    LOG.info("未找到BackupFileSender:"+sender);
                            }
                    }
            }
    }
}

 

 

    看一个备份文件发送者示例LocalBackupFileSender

 

/**
 * 将备份文件从本地一个目录复制到另一个目录
 * @author 杨尚川
 */
@Service
public class LocalBackupFileSender implements BackupFileSender{
    protected final APDPlatLogger LOG = new APDPlatLogger(getClass());

    @Override
    public void send(File file) {
        try {
            String dist = PropertyHolder.getProperty("log.backup.file.local.dir");
            LOG.info("备份文件:"+file.getAbsolutePath());
            LOG.info("目标目录:"+dist);
            FileUtils.copyFile(file, new File(dist,file.getName()));
        } catch (IOException ex) {
            LOG.info("LocalBackupFileSender失败", ex);
        }
    }
}

 

 

APDPlat托管在Github

 

© 著作权归作者所有

杨尚川

杨尚川

粉丝 1103
博文 220
码字总数 1624053
作品 12
东城
架构师
私信 提问
应用级产品开发平台 - APDPlat

APDPlat快速体验 APDPlat入门指南 APDPlat专题文章 APDPlat是Application Product Development Platform(应用级产品开发平台)的缩写。 APDPlat提供了应用容器、多模块架构、代码生成、安装...

杨尚川
2012/10/30
6.7K
0
APDPlat如何自动建库建表并初始化数据?

APDPlat共支持10种数据库:DB2、DERBY、H2、HSQL、INFORMIX、MYSQL、ORACLE、POSTGRESQL、SQLSERVER、SYBASE。 数据库的默认配置信息在文件APDPlatCore/src/main/resources/org/apdplat/db.p...

杨尚川
2014/02/08
360
0
基于word分词提供的文本相似度算法来实现通用的网页相似度检测

实现代码:基于word分词提供的文本相似度算法来实现通用的网页相似度检测 运行结果: 检查的博文数:128 1、检查博文:192本软件著作用词分析(五)用词最复杂99级,相似度分值:Simple=0.96...

杨尚川
2015/05/28
1K
0
APDPlat拓展搜索之集成ElasticSearch

APDPlat充分利用Compass的OSEM和ORM integration特性,提供了简单易用且功能强大的内置搜索特性。 APDPlat的内置搜索,在设计简洁优雅的同时,还具备了强大的实时搜索能力,用户只需用注解的...

杨尚川
2014/02/01
292
2
APDPlat拓展搜索之集成Solr

APDPlat充分利用Compass的OSEM和ORM integration特性,提供了简单易用且功能强大的内置搜索特性。 APDPlat的内置搜索,在设计简洁优雅的同时,还具备了强大的实时搜索能力,用户只需用注解的...

杨尚川
2014/02/01
662
0

没有更多内容

加载失败,请刷新页面

加载更多

nginx学习之模块

1、 stub_status模块: 用于展示nginx处理连接时的状态。 配置语法如下: Syntax:stub_status;Default:默认没有配置Context:server、location 可以编辑default.conf,加上如下配置: ...

码农实战
31分钟前
3
0
MySQL,必须掌握的6个知识点

目录 一、索引B+ Tree 原理 MySQL 索引 索引优化 索引的优点 索引的使用条件 二、查询性能优化使用 Explain 进行分析 优化数据访问 重构查询方式 三、存储引擎InnoDB MyISAM 比较 四、数据类...

李红欧巴
35分钟前
4
0
堆”和“栈

C++作为一款C语言的升级版本,具有非常强大的功能。它不但能够支持各种程序设计风格,而且还具有C语言的所有功能。我们在这里为大家介绍的是其中一个比较重要的内容,C++内存区域的基本介绍。...

SibylY
47分钟前
4
0
总结:Https

一、介绍 简单理解,https即在http协议的基础上,增加了SSL协议,保障数据传输的安全性。 它由以前的http—–>tcp,改为http——>SSL—–>tcp;https采用了共享密钥加密+公开密钥加密的方式 ...

浮躁的码农
49分钟前
6
0
数据库表与表之间的一对一、一对多、多对多关系

表1 foreign key 表2 多对一:表 1 的多条记录对应表 2 的一条记录 利用foreign key的原理我们可以制作两张表的多对多,一对一关系 多对多: 表1的多条记录可以对应表2的一条记录 表2的多条记...

Garphy
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部