springboot项目实现不停机数据迁移方案

一、背景

  1. 客户系统 MySQL 数据库中有一张表,表中数据是十亿级。
  2. 现在客户想要把这张表迁移到 Mongodb 中。
  3. 客户要求不能停机,使用户进行无感切换。

二、思路

根据背景描述,可以使用双写的思路对代码进行改造。主要步骤如下:

  1. 先开启双写
    • 双写即新表旧表同时写入数据。
    • 开启之前的数据属于存量数据,之后的数据属于增量数据。
    • 开启双写之后因为新表中没有数据,所以新表中对存量数据的修改和删除会报错。不需要理会直接忽略即可(或者也可以记录下来用于排查问题等),因为后面迁移存量数据时会一同迁移过来。
    • 注意:此时已经开始迁移增量数据了。
    • 在双写之前需要添加分布式锁,保证迁移数据过程中不会更新数据。比如:双写时先更新id=123的旧表数据,此时迁移数据开始但是没有迁移完成,同时更新新表id=123的业务开始执行,但是因为迁移没有完成导致新表中没有这条数据,导致更新失败,从而数据不一致。
  2. 读取旧表中的数据
    • 这一步表示,展示数据的时候的业务逻辑不变,还是读取旧表中的数据。
  3. 存量数据迁移
    • 把旧表中的所有数据通过代码的形式批量同步到新表中。因为步骤1中已经开始同步增量数据了,旧表中的增量数据也会重新同步到新表中,因此需要做唯一字段的校验如果插入新表时报主键冲突异常,可以直接忽略。或者先判断是否存在,如果存在就舍弃。
    • 在迁移过程中添加分布式锁。比如迁移id=1234的数据。迁移之前先加锁,如果此时有对这条数据的更新/删除操作那么就获取不到锁,直接循环阻塞。
    • 等这条数据迁移完成之后释放锁,此时更新/删除操作会获取到锁,然后执行双写。这样新表旧表中的数据理论上就可以同步。(解决了在数据迁移过程中在旧表中更新了这条数据而新表中还没有数据导致的数据不一致问题。)
    • 为了保证数据不丢和能实现断点续传,可以在旧表中增加一个字段,来标识出这条记录是否已经被迁移过,这样我们就可以在一条记录迁移成功后,把他的这个标识改了,这样如果中间失败了,就可以知道哪些数据迁移过,就只迁移这些没迁移的即可。
  4. 读取新表中的数据
    • 此时数据迁移完成,新表旧表数据已经同步,页面中的查询接口可以读取新表数据(此处建议增加一个动态开关,如果新增数据有问题可以及时回滚查旧表中的数据)。
    • 可以考虑增加一个旁路验证的逻辑–在读取新表中的数据时(可以起一个异步线程,或者MQ等)进行一次旧表的读取,然后把拿到的数据与新表的读取做对比,当发现不一致的时候,报警报出来,进行人工核对。
  5. 全量数据核对
    • 非必要。上面的步骤理论上可以保证数据一致性,如果考虑到还是有数据不一致的问题,可以增加一个全量数据核对的逻辑。通过代码一条条比对、通过工具比对等。
  6. 关闭双写
    • 用户使用一段时间后,如果功能正常将双写关闭,只把数据写到新表中。

三、代码

这里只提供伪代码供参考



import com.liran.middle.common.base.utils.ThreadPoolUtil;
import org.apache.commons.lang3.StringUtils;



public class SmoothDataMigration {

    // 数据写入类型: dualWrite-双写;newWrite-新表写入
    private String writeType;
    // 查询数据类型: oldTable-旧表;newTable-新表
    private String getType;


    /**
     * 数据写入时的主业务逻辑
     */
    public void mainBusiness() {

        // 如果开启双写,新表中同时也要写一份数据
        if ("dualWrite".equals(writeType)) {
            // 如果没有获取到锁则进行阻塞,每2秒钟尝试一次一共循环10次。10次后如果还是没有获取到锁同样也执行后面的逻辑。30秒后锁过期
            this.tryLock(2000, 主键id, 30, 10);

            // 旧表正常增删改
            oldTable.insert();
            oldTable.update();
            oldTable.delete();

            /*这里可以使用异步的形式将数据写入新表,防止对旧表操作产生影响。异步方案如下:
            1. 使用线程池做异步操作。-- 适用于可以连接到新表数据库的场景。比如新表旧表在同一个数据库中,或者可以通过多数据源连接到新表数据库。
            2. 使用 MQ 做异步操作。 -- 适用于异构系统同步,下游系统可以直接监听 MQ,将数据写入到新表。还可以分担服务器压力。
            3. 调用 API 接口调用。 -- 适用于异构系统同步,直接调用下游系统的 API 接口,将数据写入到新表。下游系统改造少。
             */
            ThreadPoolUtil.submit(() -> {
                // 未迁移数据之前,对存量数据进行更新/删除操作可能会报错,为了不影响正常的业务逻辑,可以先忽略报错。或者记录下来.
                try {
                    newTable.insert();
                    newTable.update();
                    newTable.delete();
                } catch (Exception e) {
                    errorTable.insert();
                }

                // 新表写入之后,释放锁。此时可以进行数据迁移该数据。
                this.unLock(主键id);
            });

        } else if ("newWrite".equals(writeType)) {
            // 只对新表进行操作
            newTable.insert();
            newTable.update();
            newTable.delete();
        } else {
            // 只对旧表进行操作
            oldTable.insert();
            oldTable.update();
            oldTable.delete();
        }
    }


    /**
     * 可以通过定时任务,异步进行数据迁移。
     */
    public void dataMigration() {

        // 如果没有获取到锁则进行阻塞,每2秒钟尝试一次一共循环10次。10次后如果还是没有获取到锁同样也执行后面的逻辑。30秒后锁过期
        this.tryLock(2000, 主键id, 30, 10);

        // 旧表中获取存量数据。获取的时候sql要添加order by排序,否则可能会漏掉数据。
        // 为了保证数据不丢和能实现断点续传,我们可以在旧表中增加一个字段(is_migration_success),来标识出这条记录是否已经被迁移过,这样我们就可以在一条记录迁移成功后,
        // 把他的这个标识改了,这样如果中间失败了,我们就知道哪些数据迁移过,哪些数据没迁移过,就只迁移这些没迁移的就行了。
        Object data = oldTable.select(is_migration_success = false, order by id);

        try {
            // 插入新表中,此处也可以使用多种方式。比如 MQ、API接口、多数据源等
            newTable.insert(data);
        } catch (Exception e) {
            // 如果新表中插入失败,则表示新表中已经有这个数据了。必然是增量数据,直接忽略即可。或者记录错误的数据,后续人工处理。
        }

        // 迁移成功之后,更新旧表中的标识。
        oldTable.update(is_migration_success = true);

        // 解锁
        this.unLock(主键id);
    }


    /**
     * 读取数据时路径判
     */
    public void getData() {
        // 数据迁移完成之后,从新表中获取数据。当关闭双写之后,就只从新表中获取数据,不能再切换为旧表。
        if ("newTable".equals(getType)) {
            Object newData = newTable.select();

            // 开启旁路验证,每个用户在进行查询操作时会进行校验,判断新旧表中的数据是否一致,如果不一致可以通知给管理员等,进行人工排查。
            if (开启旁路验证) {
                // 可以使用线程池或者mq做异步操作。目的只是通知管理员排查。不影响主业务逻辑。
                ThreadPoolUtil.submit(() -> {
                    Object oldData = oldTable.select();
                    if (newData != oldData) {
                        throw new RuntimeException("新表数据与旧表数据不一致,请排查!");
                    }
                });
            }
        } else {
            // 数据迁移完成之前,一直从旧表中获取数据。
            oldTable.select();
        }
    }


    
    
    
    
    
    
    
    
//=======================以下为分布式锁工具============================================================================================


    /**
     * 取锁入口
     *
     * @param waitTime      等待时间 单位毫秒
     * @param key           页面id
     * @param expireTime    过期时间 单位秒
     * @param maxRetryTimes 最大重试次数
     * @return
     */
    public boolean tryLock(long waitTime, String key, Integer expireTime, int maxRetryTimes) {
        String keyName = "test:" + key;
        boolean flag = false;
        if (getLock(keyName, expireTime)) {
            flag = true;
        } else {
            //重试取锁
            if (retryLock(waitTime, keyName, expireTime, maxRetryTimes)) {
                flag = true;
            }
        }
        return flag;
    }

    /**
     * 重试锁机制
     * 在获取锁失败后,等待指定时间后重新尝试获取锁
     *
     * @param waitTime   等待时间 单位毫秒
     * @param keyName    锁名称
     * @param expireTime 过期时间 单位秒
     * @return true 成功取得锁,false 获取锁失败
     */
    private boolean retryLock(long waitTime, String keyName, Integer expireTime, int maxRetryTimes) {
        //重试次数
        int retryTimes = 1;
        try {
            while (retryTimes <= maxRetryTimes) {
                //在等待指定时间后重新拿锁
                Thread.sleep(waitTime);
                if (getLock(keyName, expireTime)) {
                    return true;
                }
                retryTimes++;
            }
            return false;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获得分布式锁
     *
     * @param keyName    锁名称
     * @param expireTime 过期时间 单位秒
     * @return true 成功取得锁,false 获取锁失败
     */
    public boolean getLock(String keyName, Integer expireTime) {
        Boolean result = false;
        try {
            if (reids.tryLock(keyName, expireTime)) {
                result = true;
            }
        } catch (Exception e) {
            return false;
        }
        return result;
    }

    /**
     * 解锁
     *
     * @param key 锁名
     */
    public void unLock(String key) {
        if (StringUtils.isEmpty(key)) {
            return;
        }
        String keyName = "test:" + key;
        try {
            reids.unLock(keyName);
        } catch (Exception e) {
            logger.error("error unLock", e);
        }
    }
}

四、注意

  1. 此方案逻辑上可以保证不停机的数据迁移。
  2. 真实使用时要做好回滚的方案,保证旧逻辑的正确性。
<think>我们正在讨论Spring Boot项目的高可用部署。根据引用内容,高可用部署通常涉及多个方面:服务发现、负载均衡、故障转移、容器化部署、云原生技术等。 参考引用[2]提到Spring Boot结合Spring Cloud可以搭建高可用分布式系统,包括服务发现、熔断降级、API网关等。 另外,引用[1]和[3]提到了部署方式,包括容器化、Kubernetes集群部署,以及传统部署方式。 高可用部署最佳实践通常包括: 1. 使用微服务架构:将应用拆分为多个微服务,每个服务可以独立部署和扩展。 2. 服务发现与注册:使用如Eureka、Consul、Nacos等服务注册中心,使得服务实例可以动态注册和发现。 3. 负载均衡:通过服务网关(如Spring Cloud Gateway)或客户端负载均衡(如Ribbon)来分发请求。 4. 熔断与降级:使用Hystrix或Resilience4j等组件防止服务雪崩。 5. 容器化与编排:将应用打包为Docker容器,并使用Kubernetes进行编排管理,实现自动扩缩容和故障恢复。 6. 配置中心:使用Spring Cloud Config或Nacos等统一管理配置,实现配置的动态更新。 7. 健康检查与监控:通过Actuator暴露健康检查端点,并集成监控系统(如Prometheus)和日志系统(如ELK)。 8. 自动化部署:通过CI/CD流水线实现自动化部署,减少人为错误。 针对Spring Boot项目,具体实施步骤: 1. 服务拆分:根据业务领域将单体应用拆分为多个微服务(每个是一个Spring Boot项目)。 2. 服务注册与发现: - 在项目中引入Spring Cloud Netflix Eureka或Spring Cloud Alibaba Nacos等依赖。 - 配置服务注册中心地址,并在应用启动时自动注册。 - 引用[5]提到外部Tomcat部署时可能无法自动注册到Nacos的问题,需要特殊处理(如手动注册或调整事件监听)。 3. 负载均衡: - 在服务消费者端使用@LoadBalanced注解的RestTemplate或使用OpenFeign进行声明式服务调用,实现客户端负载均衡。 4. 熔断降级: - 使用@HystrixCommand注解或Resilience4j的断路器模式。 5. 容器化部署: - 为每个服务编写Dockerfile,构建Docker镜像。 - 使用Kubernetes部署,创建Deployment(控制副本数量)和Service(暴露服务)。 - 设置资源限制和健康检查(livenessProbe和readinessProbe)。 6. 配置中心: - 将配置从本地application.properties迁移到配置中心,实现配置的集中管理和动态更新。 7. 健康检查与监控: - 开启Spring Boot Actuator端点(如/actuator/health, /actuator/info),并集成监控系统。 8. 自动化部署: - 使用Jenkins、GitLab CI等工具,实现从代码提交到构建、测试、部署的自动化流程。 9. 高可用数据库:使用主从复制、分库分表等策略确保数据库高可用。 10. 无状态设计:确保应用实例无状态,会话数据存储到外部存储(如Redis),便于水平扩展。 11. 滚动更新与蓝绿部署:在Kubernetes中可以通过Deployment的滚动更新策略实现零宕机部署,或者采用蓝绿部署、金丝雀发布等高级部署策略。 12. 安全加固:遵循引用[1]中的安全实践,如最小权限原则、密钥管理等。 注意事项: - 在容器化部署时,确保应用能够正确处理SIGTERM信号,实现优雅关闭。 - 使用外部Tomcat部署时,注意引用[5]中提到的事件监听问题,可能需要手动触发注册或调整应用启动流程。 综上所述,Spring Boot项目实现高可用部署的最佳实践是采用微服务架构,结合容器化和Kubernetes编排,并利用Spring Cloud生态提供的服务治理能力。 下面我们生成相关问题: 1. 如何解决Spring Boot应用在外部Tomcat部署时无法自动注册到Nacos的问题? 2. 在Kubernetes中如何配置Spring Boot应用的健康检查? 3. Spring Cloud Gateway如何实现负载均衡和熔断? 4. 如何设计Spring Boot微服务的安全认证机制? 5. Spring Boot项目如何实现蓝绿部署?</think>### Spring Boot 项目高可用部署最佳实践 实现 Spring Boot 高可用部署需结合**容器化、服务治理、负载均衡**等技术,以下是关键实践: --- #### 1. **容器化与集群部署** - **容器封装**: 使用 Docker 打包应用,确保环境一致性。 ```dockerfile FROM openjdk:17 COPY target/app.jar /app.jar ENTRYPOINT ["java","-jar","/app.jar"] ``` - **集群编排**: 通过 Kubernetes 管理多实例,实现自动扩缩容和故障转移[^2]。 ```yaml # Kubernetes Deployment 示例 replicas: 3 # 至少3个副本保证高可用 readinessProbe: httpGet: path: /actuator/health port: 8080 ``` --- #### 2. **服务发现与负载均衡** - **注册中心**: 集成 **Nacos** 或 **Eureka** 实现服务注册与发现[^2][^5]。 ```yaml # application.yml spring: cloud: nacos: discovery: server-addr: nacos-cluster:8848 # Nacos集群地址 ``` - **负载均衡**: 使用 **Spring Cloud LoadBalancer** 或 **Kubernetes Service** 分发流量[^2]。 --- #### 3. **熔断与限流** - **熔断降级**: 通过 **Resilience4j** 或 **Sentinel** 防止雪崩效应。 ```java @CircuitBreaker(name = "backendA", fallbackMethod = "fallback") public String callService() { ... } ``` - **限流策略**: 配置 QPS 限制,保护核心服务[^1]。 --- #### 4. **配置中心与动态刷新** - **集中管理配置**: 使用 **Nacos Config** 或 **Spring Cloud Config** 动态更新参数[^2][^4]。 ```yaml spring: config: import: nacos:app-config.yaml # 远程配置 ``` - **热部署支持**: 结合 `spring-boot-devtools` 实现部分热更新[^4]。 --- #### 5. **健康检查与监控** - **探针配置**: Kubernetes 通过 `/actuator/health` 监控实例状态[^1][^3]。 - **可视化监控**: 集成 **Prometheus + Grafana** 监控 JVM、吞吐量等指标[^1]。 --- #### 6. **安全加固** - **最小权限原则**: 容器以非 root 用户运行[^1]。 - **密钥管理**: 使用 **Kubernetes Secrets** 或 **HashiCorp Vault** 管理敏感数据[^1]。 --- #### 7. **自动化与 CI/CD** - **流水线设计**: Jenkins/GitLab CI 实现自动构建、测试、部署[^1]。 - **滚动更新策略**: Kubernetes 逐步替换旧实例,确保零停机[^2]。 --- ### 关键注意事项 1. **避免单点故障**: - 数据库/中间件使用主从集群(如 Redis Sentinel、MySQL Group Replication)。 2. **优雅停机**: - 处理 `SIGTERM` 信号,确保请求完成再终止进程[^3]。 3. **外部容器适配**: - WAR 部署时需手动触发注册事件(解决 Nacos 未注册问题)[^5]。 > **实践总结**: > 云原生场景首选 **Kubernetes + Spring Cloud** 方案;传统环境可用 **Nginx + Tomcat 集群**。核心是**冗余设计 + 自动故障恢复**。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栗然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值