在系统升级过程中,数据的平滑迁移是关键的一部分,目的是在新旧系统同时运行的过程中,确保数据一致性和可用性。以下是一个实际场景案例及相应的实例代码。
实际场景案例
场景:电商平台订单服务升级
假设一个电商平台需要升级订单服务的数据库结构,将 order
表中的 customer_name
字段拆分为 first_name
和 last_name
两个字段,同时新系统引入了一个新的服务端点来操作订单。
数据平滑迁移的步骤
- 添加冗余字段(兼容阶段)
- 在现有数据库中添加新的字段
first_name
和last_name
。 - 继续支持旧系统
customer_name
的读写。
- 在现有数据库中添加新的字段
- 双写阶段
- 修改业务逻辑,在更新
customer_name
时,同时更新first_name
和last_name
。 - 允许新字段参与数据查询。
- 修改业务逻辑,在更新
- 切换阶段
- 逐步切换到使用
first_name
和last_name
代替customer_name
。
- 逐步切换到使用
- 清理阶段
- 在验证所有业务无误后,移除旧字段
customer_name
。
- 在验证所有业务无误后,移除旧字段
- 旧架构 :
Order
服务存储customerName
。 - 新架构 :
Order
服务存储firstName
和lastName
,并通过Customer
服务实时获取客户信息。 - 目标:
- 实现旧数据迁移。
- 支持双写逻辑。
- 灰度切换到新系统。
详细实现代码
数据库表结构
Order 表:
sql
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customer_name VARCHAR(100), -- 旧字段
first_name VARCHAR(50), -- 新字段
last_name VARCHAR(50), -- 新字段
customer_id BIGINT -- 外键关联 Customer
);
Customer 表:
sql
CREATE TABLE customers (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50)
);
Order 服务
实体类
java
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customerName; // 旧字段
private String firstName; // 新字段
private String lastName; // 新字段
private Long customerId; // 关联 Customer
// Getters and setters
}
Repository 接口
java
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByFirstNameAndLastName(String firstName, String lastName);
}
服务层
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CustomerClient customerClient; // Feign 客户端与 Customer 服务交互
// 创建订单,支持新旧逻辑
@Transactional
public Order createOrder(String customerName) {
// 调用 Customer 服务获取信息
CustomerResponse customer = customerClient.getCustomerByName(customerName);
Order order = new Order();
order.setCustomerName(customerName); // 旧字段
// 填充新字段
order.setFirstName(customer.getFirstName());
order.setLastName(customer.getLastName());
order.setCustomerId(customer.getId());
return orderRepository.save(order);
}
// 查询订单
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow(() -> new RuntimeException("Order not found"));
}
// 更新订单
@Transactional
public Order updateOrder(Long id, String customerName) {
CustomerResponse customer = customerClient.getCustomerByName(customerName);
Order order = getOrder(id);
order.setCustomerName(customerName); // 更新旧字段
order.setFirstName(customer.getFirstName()); // 同步新字段
order.setLastName(customer.getLastName());
order.setCustomerId(customer.getId());
return orderRepository.save(order);
}
// 批量迁移旧数据
@Transactional
public void migrateOldData() {
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
if (order.getFirstName() == null || order.getLastName() == null) {
// 调用 Customer 服务补全信息
CustomerResponse customer = customerClient.getCustomerByName(order.getCustomerName());
order.setFirstName(customer.getFirstName());
order.setLastName(customer.getLastName());
order.setCustomerId(customer.getId());
orderRepository.save(order);
}
}
}
}
Customer 服务
实体类
java
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
// Getters and setters
}
Repository 接口
java
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Customer findByFirstNameAndLastName(String firstName, String lastName);
}
服务层
java
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
public Customer getCustomer(Long id) {
return customerRepository.findById(id).orElseThrow(() -> new RuntimeException("Customer not found"));
}
public Customer createCustomer(String firstName, String lastName) {
Customer customer = new Customer();
customer.setFirstName(firstName);
customer.setLastName(lastName);
return customerRepository.save(customer);
}
}
Feign 客户端(Order 服务调用 Customer 服务)
java
@FeignClient(name = "customer-service")
public interface CustomerClient {
@GetMapping("/customers/name")
CustomerResponse getCustomerByName(@RequestParam String name);
}
Feign 响应对象
java
public class CustomerResponse {
private Long id;
private String firstName;
private String lastName;
// Getters and setters
}
批量迁移工具
在启动时执行批量数据迁移。
java
@Component
public class DataMigrationTask implements CommandLineRunner {
@Autowired
private OrderService orderService;
@Override
public void run(String... args) throws Exception {
orderService.migrateOldData();
System.out.println("Data migration completed.");
}
}
控制器层
Order 控制器:
java
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestParam String customerName) {
Order order = orderService.createOrder(customerName);
return ResponseEntity.ok(order);
}
@PutMapping("/{id}")
public ResponseEntity<Order> updateOrder(@PathVariable Long id, @RequestParam String customerName) {
Order order = orderService.updateOrder(id, customerName);
return ResponseEntity.ok(order);
}
}
Customer 控制器:
java
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
@PostMapping
public ResponseEntity<Customer> createCustomer(@RequestParam String firstName, @RequestParam String lastName) {
Customer customer = customerService.createCustomer(firstName, lastName);
return ResponseEntity.ok(customer);
}
}
灰度发布策略
- 启动阶段 :
- Order 服务启用双写逻辑。
- Customer 服务接收新数据写入。
- 切换阶段 :
- 对旧数据进行批量迁移。
- 验证新系统接口的稳定性。
- 清理阶段 :
- 停止使用
customerName
字段。 - 更新所有业务逻辑到新字段。
- 停止使用
通过引入跨服务交互和批量迁移的逻辑,这个案例模拟了真实环境中的平滑迁移场景。