Administrator
发布于 2026-03-16 / 1 阅读
0
0

MyBatis-Plus高阶实战与信创改造指南

# MyBatis-Plus高阶实战与信创改造指南

> 🎯 从熟练到精通:MyBatis-Plus进阶 + 分库分表 + 信创数据库迁移  
> 涵盖:高级查询、性能优化、分库分表实战、MySQL迁移达梦数据库

---

## 📚 目录

1. [MyBatis-Plus高阶使用技巧](#一mybatis-plus高阶使用技巧)
2. [分库分表实战](#二分库分表实战)
3. [分表数据一致性保证](#三分表数据一致性保证)
4. [数据库切换实战](#四数据库切换实战)
5. [信创达梦数据库改造](#五信创达梦数据库改造)
6. [面试常见问题](#六面试常见问题)

---

# 一、MyBatis-Plus高阶使用技巧

## 1.1 链式查询(LambdaQueryWrapper)

**基础用法:**
```java
// ❌ 字符串方式(容易写错,不利于重构)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三").gt("age", 18);

// ✅ Lambda方式(类型安全,支持重构)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
       .gt(User::getAge, 18);

// ✅ 更简洁的写法
List<User> users = userMapper.selectList(
    Wrappers.<User>lambdaQuery()
        .eq(User::getName, "张三")
        .gt(User::getAge, 18)
);
```

**企业级复杂查询场景:订单查询**
```java
@Service
public class OrderQueryService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 多条件动态查询
     * 
     * 业务场景:订单列表查询,支持按状态、时间、用户等多维度筛选
     */
    public Page<Order> queryOrders(OrderQueryRequest request) {
        LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery()
            // 1. 状态(可选)
            .eq(request.getStatus() != null, Order::getStatus, request.getStatus())
            
            // 2. 用户ID(可选)
            .eq(request.getUserId() != null, Order::getUserId, request.getUserId())
            
            // 3. 订单号模糊查询
            .like(StrUtil.isNotBlank(request.getOrderNo()), 
                  Order::getOrderNo, request.getOrderNo())
            
            // 4. 时间范围
            .between(request.getStartTime() != null && request.getEndTime() != null,
                     Order::getCreateTime, request.getStartTime(), request.getEndTime())
            
            // 5. 金额范围
            .ge(request.getMinAmount() != null, Order::getTotalAmount, request.getMinAmount())
            .le(request.getMaxAmount() != null, Order::getTotalAmount, request.getMaxAmount())
            
            // 6. 排序
            .orderByDesc(Order::getCreateTime);
        
        // 分页查询
        Page<Order> page = new Page<>(request.getPageNum(), request.getPageSize());
        return orderMapper.selectPage(page, wrapper);
    }
    
    /**
     * IN查询优化
     * 
     * 业务场景:根据多个订单号批量查询
     */
    public List<Order> batchQueryByOrderNos(List<String> orderNos) {
        if (CollectionUtils.isEmpty(orderNos)) {
            return Collections.emptyList();
        }
        
        // MyBatis-Plus会自动分批查询(默认1000条一批)
        return orderMapper.selectList(
            Wrappers.<Order>lambdaQuery()
                .in(Order::getOrderNo, orderNos)
        );
    }
    
    /**
     * 复杂嵌套查询
     * 
     * SQL: WHERE (status = 1 OR status = 2) AND (user_id IN (...) AND create_time > ...)
     */
    public List<Order> complexQuery(List<Long> userIds, Date startTime) {
        return orderMapper.selectList(
            Wrappers.<Order>lambdaQuery()
                .and(wrapper -> wrapper
                    .eq(Order::getStatus, 1)
                    .or()
                    .eq(Order::getStatus, 2)
                )
                .and(wrapper -> wrapper
                    .in(Order::getUserId, userIds)
                    .gt(Order::getCreateTime, startTime)
                )
        );
    }
}
```

## 1.2 自定义SQL增强

**场景:需要自定义SQL,但想用MyBatis-Plus的分页**

```java
// Mapper接口
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    
    /**
     * 自定义SQL + MyBatis-Plus分页
     * 
     * 注意:
     * 1. 第一个参数必须是Page
     * 2. 返回类型是IPage
     * 3. Wrapper可以动态拼接条件
     */
    IPage<OrderVO> selectOrderPage(
        Page<OrderVO> page,
        @Param(Constants.WRAPPER) Wrapper<Order> wrapper
    );
    
    /**
     * 关联查询(订单 + 用户信息)
     */
    List<OrderDetailVO> selectOrderWithUser(@Param("orderIds") List<Long> orderIds);
}
```

```xml
<!-- OrderMapper.xml -->
<mapper namespace="cn.ufood.fny.mall.mapper.OrderMapper">
    
    <!-- 自定义分页查询 -->
    <select id="selectOrderPage" resultType="cn.ufood.fny.mall.vo.OrderVO">
        SELECT 
            o.id,
            o.order_no,
            o.total_amount,
            o.status,
            u.name as user_name,
            u.phone as user_phone
        FROM orders o
        LEFT JOIN user u ON o.user_id = u.id
        ${ew.customSqlSegment}  <!-- 动态条件 -->
    </select>
    
    <!-- 复杂关联查询 -->
    <select id="selectOrderWithUser" resultType="cn.ufood.fny.mall.vo.OrderDetailVO">
        SELECT 
            o.*,
            u.name,
            u.phone,
            GROUP_CONCAT(oi.product_name) as product_names
        FROM orders o
        LEFT JOIN user u ON o.user_id = u.id
        LEFT JOIN order_item oi ON o.id = oi.order_id
        WHERE o.id IN
        <foreach collection="orderIds" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
        GROUP BY o.id
    </select>
</mapper>
```

```java
// 使用示例
public IPage<OrderVO> queryOrderPage(OrderQueryRequest request) {
    Page<OrderVO> page = new Page<>(request.getPageNum(), request.getPageSize());
    
    // 构建动态条件
    LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery()
        .eq(request.getStatus() != null, Order::getStatus, request.getStatus())
        .ge(request.getStartTime() != null, Order::getCreateTime, request.getStartTime());
    
    // MyBatis-Plus自动处理分页
    return orderMapper.selectOrderPage(page, wrapper);
}
```

## 1.3 批量操作优化

```java
@Service
public class OrderBatchService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 批量插入(推荐方式1:saveBatch)
     * 
     * 优点:
     * 1. 自动分批(默认1000条)
     * 2. 自动填充createTime等字段
     * 3. 支持主键回填
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchInsert1(List<Order> orders) {
        // MyBatis-Plus自动分批
        orderService.saveBatch(orders);
    }
    
    /**
     * 批量插入(推荐方式2:insertBatchSomeColumn)
     * 
     * 优点:
     * 1. 性能更好(单条SQL插入多行)
     * 2. 可以指定字段
     * 
     * 需要配置:
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchInsert2(List<Order> orders) {
        // 使用扩展的批量插入
        orderService.getBaseMapper().insertBatchSomeColumn(orders);
    }
    
    /**
     * 批量更新(方式1:updateBatchById)
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdate1(List<Order> orders) {
        orderService.updateBatchById(orders);
    }
    
    /**
     * 批量更新(方式2:自定义SQL)
     * 
     * 场景:更新指定字段,性能更好
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdateStatus(List<Long> orderIds, Integer status) {
        orderMapper.update(null,
            Wrappers.<Order>lambdaUpdate()
                .set(Order::getStatus, status)
                .in(Order::getId, orderIds)
        );
    }
    
    /**
     * 批量删除(逻辑删除)
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchDelete(List<Long> ids) {
        // 如果配置了逻辑删除,这里是更新is_delete字段
        orderService.removeByIds(ids);
    }
}
```

**批量插入性能对比:**
```
数据量:10000条

方式1:逐条插入
for (Order order : orders) {
    orderMapper.insert(order);
}
耗时:~30秒

方式2:saveBatch(分批1000条)
orderService.saveBatch(orders);
耗时:~5秒

方式3:单SQL插入多行
INSERT INTO orders VALUES (...), (...), (...)
耗时:~1秒
```

## 1.4 自动填充(MetaObjectHandler)

```java
/**
 * 自动填充配置
 * 
 * 业务场景:
 * - 创建时自动填充createTime、createBy
 * - 更新时自动填充updateTime、updateBy
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    
    @Override
    public void insertFill(MetaObject metaObject) {
        log.debug("开始插入填充...");
        
        // 自动填充创建时间
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        
        // 自动填充创建人(从ThreadLocal获取当前用户)
        Long userId = UserContext.getCurrentUserId();
        if (userId != null) {
            this.strictInsertFill(metaObject, "createBy", Long.class, userId);
        }
        
        // 自动填充更新时间
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateBy", Long.class, userId);
    }
    
    @Override
    public void updateFill(MetaObject metaObject) {
        log.debug("开始更新填充...");
        
        // 自动填充更新时间
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        
        // 自动填充更新人
        Long userId = UserContext.getCurrentUserId();
        if (userId != null) {
            this.strictUpdateFill(metaObject, "updateBy", Long.class, userId);
        }
    }
}

// 实体类配置
@Data
@TableName("orders")
public class Order {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String orderNo;
    
    @TableField(fill = FieldFill.INSERT)  // 插入时填充
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT)
    private Long createBy;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)  // 插入和更新都填充
    private LocalDateTime updateTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateBy;
}
```

## 1.5 逻辑删除

```java
/**
 * 全局逻辑删除配置
 */
@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

// application.yml
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDelete  # 全局逻辑删除字段
      logic-delete-value: 1         # 删除值
      logic-not-delete-value: 0     # 未删除值

// 实体类
@Data
public class Order {
    @TableLogic  // 逻辑删除标识
    private Integer isDelete;
}

// 使用
orderService.removeById(1L);  // 实际执行:UPDATE orders SET is_delete=1 WHERE id=1
orderService.list();          // 自动过滤:SELECT * FROM orders WHERE is_delete=0
```

## 1.6 多租户(SaaS场景)

```java
/**
 * 多租户拦截器
 * 
 * 业务场景:SaaS平台,每个租户只能看到自己的数据
 */
@Configuration
public class TenantConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 多租户插件
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
        tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
            
            @Override
            public Expression getTenantId() {
                // 从ThreadLocal获取租户ID
                Long tenantId = TenantContext.getCurrentTenantId();
                return new LongValue(tenantId);
            }
            
            @Override
            public String getTenantIdColumn() {
                return "tenant_id";  // 租户字段名
            }
            
            @Override
            public boolean ignoreTable(String tableName) {
                // 哪些表不需要租户隔离
                return Arrays.asList("sys_user", "sys_config").contains(tableName);
            }
        });
        
        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

// 效果:
// SELECT * FROM orders
// 自动拼接:SELECT * FROM orders WHERE tenant_id = 1001

// INSERT INTO orders (order_no, amount)
// 自动拼接:INSERT INTO orders (order_no, amount, tenant_id) VALUES (?, ?, 1001)
```

---

# 二、分库分表实战

## 2.1 为什么要分库分表?

**单表数据量到多少需要分表?**

| 数据量 | 查询性能 | 建议 |
|--------|---------|------|
| < 100万 | 正常 | 无需分表,加索引即可 |
| 100万 - 500万 | 开始变慢 | 考虑分表 |
| > 1000万 | 很慢 | 必须分表 |

**业务场景举例:订单表**
```
用户量:1000万
每个用户平均20个订单
订单总量:2亿

如果不分表:
- 单表2亿数据
- 即使有索引,查询也很慢
- 数据备份困难
- 影响整体性能

分表后:
- 按月分表:order_202401, order_202402...
- 每张表约1600万(假设均匀分布)
- 查询只需扫描当月表
- 历史数据可以归档
```

## 2.2 分库分表方案选择

| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **ShardingSphere** | 功能全、社区活跃 | 学习成本高 | 复杂场景 |
| **MyCAT** | 独立中间件 | 额外维护成本 | 大型项目 |
| **手动分表** | 灵活、可控 | 开发成本高 | 简单场景 |

## 2.3 ShardingSphere分表实战

### 2.3.1 Maven依赖

```xml
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>
```

### 2.3.2 按月分表配置

```yaml
# application-sharding.yml
spring:
  shardingsphere:
    datasource:
      names: ds0
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db?useSSL=false
        username: root
        password: root
    
    rules:
      sharding:
        tables:
          # 订单表分片规则
          orders:
            actual-data-nodes: ds0.orders_$->{202401..202412}  # 12张表
            table-strategy:
              standard:
                sharding-column: create_time  # 分片字段
                sharding-algorithm-name: order-month-algorithm
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
        
        # 分片算法
        sharding-algorithms:
          order-month-algorithm:
            type: CLASS_BASED
            props:
              strategy: STANDARD
              algorithmClassName: cn.ufood.fny.sharding.OrderMonthShardingAlgorithm
        
        # 主键生成策略
        key-generators:
          snowflake:
            type: SNOWFLAKE
    
    props:
      sql-show: true  # 打印SQL
```

### 2.3.3 自定义分片算法

```java
/**
 * 按月分表算法
 * 
 * 分表规则:
 * - 2024-01的数据 → orders_202401
 * - 2024-02的数据 → orders_202402
 */
public class OrderMonthShardingAlgorithm implements StandardShardingAlgorithm<Date> {
    
    @Override
    public String doSharding(Collection<String> availableTables, 
                            PreciseShardingValue<Date> shardingValue) {
        Date createTime = shardingValue.getValue();
        String suffix = DateUtil.format(createTime, "yyyyMM");
        String tableName = "orders_" + suffix;
        
        // 检查表是否存在
        if (availableTables.contains(tableName)) {
            return tableName;
        }
        
        // 表不存在,抛异常或创建表
        throw new IllegalArgumentException("表不存在: " + tableName);
    }
    
    @Override
    public Collection<String> doSharding(Collection<String> availableTables,
                                        RangeShardingValue<Date> shardingValue) {
        // 范围查询:查询2024-01到2024-03的数据
        Range<Date> range = shardingValue.getValueRange();
        Date start = range.lowerEndpoint();
        Date end = range.upperEndpoint();
        
        // 计算需要查询哪些表
        Set<String> result = new HashSet<>();
        LocalDate current = LocalDateTimeUtil.of(start).toLocalDate();
        LocalDate endDate = LocalDateTimeUtil.of(end).toLocalDate();
        
        while (!current.isAfter(endDate)) {
            String suffix = current.format(DateTimeFormatter.ofPattern("yyyyMM"));
            String tableName = "orders_" + suffix;
            if (availableTables.contains(tableName)) {
                result.add(tableName);
            }
            current = current.plusMonths(1);
        }
        
        return result;
    }
}
```

### 2.3.4 使用示例

```java
@Service
public class OrderShardingService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 插入数据(自动路由到对应月份的表)
     */
    public void createOrder(Order order) {
        order.setCreateTime(new Date());
        // ShardingSphere自动计算:插入到orders_202401
        orderMapper.insert(order);
    }
    
    /**
     * 精确查询(单表)
     */
    public Order getOrderByCreateTime(String orderNo, Date createTime) {
        // ShardingSphere自动路由到orders_202401
        return orderMapper.selectOne(
            Wrappers.<Order>lambdaQuery()
                .eq(Order::getOrderNo, orderNo)
                .eq(Order::getCreateTime, createTime)
        );
    }
    
    /**
     * 范围查询(跨表)
     */
    public List<Order> queryOrdersByTimeRange(Date startTime, Date endTime) {
        // ShardingSphere自动查询多张表并合并结果
        // 实际执行:
        // SELECT * FROM orders_202401 WHERE create_time BETWEEN ? AND ?
        // UNION ALL
        // SELECT * FROM orders_202402 WHERE create_time BETWEEN ? AND ?
        // UNION ALL
        // SELECT * FROM orders_202403 WHERE create_time BETWEEN ? AND ?
        return orderMapper.selectList(
            Wrappers.<Order>lambdaQuery()
                .between(Order::getCreateTime, startTime, endTime)
        );
    }
    
    /**
     * 分页查询(跨表)
     * 
     * 注意:跨表分页性能较差,建议限制查询范围
     */
    public Page<Order> pageQuery(Date startTime, Date endTime, int pageNum, int pageSize) {
        Page<Order> page = new Page<>(pageNum, pageSize);
        return orderMapper.selectPage(page,
            Wrappers.<Order>lambdaQuery()
                .between(Order::getCreateTime, startTime, endTime)
                .orderByDesc(Order::getCreateTime)
        );
    }
}
```

### 2.3.5 自动建表

```java
/**
 * 自动创建分表
 * 
 * 场景:每月初自动创建下个月的表
 */
@Component
@Slf4j
public class AutoCreateTableTask {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 每月1号凌晨2点创建下个月的表
     */
    @Scheduled(cron = "0 0 2 1 * ?")
    public void createNextMonthTable() {
        LocalDate nextMonth = LocalDate.now().plusMonths(1);
        String suffix = nextMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
        String tableName = "orders_" + suffix;
        
        // 检查表是否存在
        String checkSql = "SHOW TABLES LIKE '" + tableName + "'";
        List<Map<String, Object>> result = jdbcTemplate.queryForList(checkSql);
        
        if (result.isEmpty()) {
            // 创建表(复制上个月表结构)
            String createSql = "CREATE TABLE " + tableName + " LIKE orders_template";
            jdbcTemplate.execute(createSql);
            
            log.info("自动创建分表成功: {}", tableName);
        }
    }
}
```

## 2.4 按用户ID分表(Hash分表)

```yaml
spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: ds0.orders_$->{0..9}  # 10张表
            table-strategy:
              standard:
                sharding-column: user_id  # 按用户ID分片
                sharding-algorithm-name: order-hash-algorithm
        
        sharding-algorithms:
          order-hash-algorithm:
            type: HASH_MOD  # 哈希取模
            props:
              sharding-count: 10  # 10张表
```

```java
// 使用
public void createOrder(Order order) {
    order.setUserId(123456L);
    // 自动计算:123456 % 10 = 6
    // 插入到:orders_6
    orderMapper.insert(order);
}
```

---

# 三、分表数据一致性保证

## 3.1 跨表查询问题

**问题**:订单分表后,如何按订单号查询?

```java
/**
 * 方案1:订单号中包含路由信息
 */
public String generateOrderNo(Long userId, Date createTime) {
    // 订单号格式:yyyyMMdd + userId后4位 + 随机数
    String datePart = DateUtil.format(createTime, "yyyyMMdd");
    String userPart = String.format("%04d", userId % 10000);
    String randomPart = RandomUtil.randomNumbers(6);
    
    return datePart + userPart + randomPart;
    // 例如:202401010123456789
    //      2024-01-01 + 用户0123 + 456789
}

// 查询时可以解析订单号,确定查询哪张表
public Order getOrderByNo(String orderNo) {
    // 解析日期 → 确定月份表
    String dateStr = orderNo.substring(0, 8);
    String tableSuffix = orderNo.substring(0, 6);  // 202401
    
    // 查询指定表
    return orderMapper.selectOne(
        Wrappers.<Order>lambdaQuery()
            .eq(Order::getOrderNo, orderNo)
            .last("FROM orders_" + tableSuffix)  // 指定表名
    );
}

/**
 * 方案2:建立路由表
 */
CREATE TABLE order_route (
    order_no VARCHAR(32) PRIMARY KEY,
    table_name VARCHAR(32),
    create_time DATETIME,
    INDEX idx_create_time (create_time)
);

// 插入订单时,同时插入路由表
@Transactional
public void createOrder(Order order) {
    // 1. 插入订单
    orderMapper.insert(order);
    
    // 2. 插入路由信息
    OrderRoute route = new OrderRoute();
    route.setOrderNo(order.getOrderNo());
    route.setTableName("orders_202401");
    route.setCreateTime(order.getCreateTime());
    orderRouteMapper.insert(route);
}

// 查询时先查路由表
public Order getOrderByNo(String orderNo) {
    // 1. 查路由表
    OrderRoute route = orderRouteMapper.selectById(orderNo);
    
    // 2. 查具体表
    return orderMapper.selectOne(
        Wrappers.<Order>lambdaQuery()
            .eq(Order::getOrderNo, orderNo)
            .last("FROM " + route.getTableName())
    );
}
```

## 3.2 分布式事务

```java
/**
 * Seata分布式事务
 * 
 * 场景:下单时需要操作多张分表
 */
@Service
public class OrderDistributedTransactionService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private OrderItemMapper orderItemMapper;
    
    /**
     * 下单:订单主表 + 订单明细表
     * 
     * 订单主表:按月分表
     * 订单明细表:按订单ID Hash分表
     */
    @GlobalTransactional  // Seata全局事务
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order, List<OrderItem> items) {
        // 1. 插入主订单(路由到orders_202401)
        orderMapper.insert(order);
        
        // 2. 插入订单明细(可能分散到多张表)
        for (OrderItem item : items) {
            item.setOrderId(order.getId());
            orderItemMapper.insert(item);
        }
        
        // 如果任何一步失败,Seata保证所有表都回滚
    }
}
```

## 3.3 数据迁移与归档

```java
/**
 * 历史数据归档
 * 
 * 场景:6个月前的订单归档到历史库
 */
@Component
@Slf4j
public class OrderArchiveTask {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Autowired
    private JdbcTemplate archiveJdbcTemplate;  // 归档库
    
    /**
     * 每月归档6个月前的数据
     */
    @Scheduled(cron = "0 0 3 1 * ?")
    public void archiveOldOrders() {
        LocalDate sixMonthsAgo = LocalDate.now().minusMonths(6);
        String tableSuffix = sixMonthsAgo.format(DateTimeFormatter.ofPattern("yyyyMM"));
        String sourceTable = "orders_" + tableSuffix;
        String targetTable = "orders_archive_" + tableSuffix;
        
        try {
            // 1. 复制数据到归档库
            String copySql = String.format(
                "INSERT INTO archive_db.%s SELECT * FROM order_db.%s",
                targetTable, sourceTable
            );
            archiveJdbcTemplate.execute(copySql);
            
            // 2. 验证数据
            Long sourceCount = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM " + sourceTable, Long.class);
            Long targetCount = archiveJdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM " + targetTable, Long.class);
            
            if (!sourceCount.equals(targetCount)) {
                throw new RuntimeException("归档数据不一致");
            }
            
            // 3. 删除原表数据
            jdbcTemplate.execute("DROP TABLE " + sourceTable);
            
            log.info("订单归档成功: {} -> {}, 数量: {}", 
                sourceTable, targetTable, sourceCount);
            
        } catch (Exception e) {
            log.error("订单归档失败", e);
            // 发送告警
        }
    }
}
```

---

# 四、数据库切换实战

## 4.1 MySQL切换PostgreSQL

**差异点梳理:**

| 差异项 | MySQL | PostgreSQL |
|--------|-------|-----------|
| **自增主键** | AUTO_INCREMENT | SERIAL / SEQUENCE |
| **字符串拼接** | CONCAT() | \|\| |
| **分页** | LIMIT offset, size | LIMIT size OFFSET offset |
| **日期函数** | NOW(), DATE_FORMAT() | NOW(), TO_CHAR() |
| **反引号** | \`table\` | "table" |
| **IF函数** | IF(condition, a, b) | CASE WHEN condition THEN a ELSE b END |

**改造步骤:**

```java
// 1. 抽象SQL方言接口
public interface SqlDialect {
    String getPageSql(String sql, int offset, int size);
    String getDateFormatFunction(String column, String format);
    String getIfFunction(String condition, String trueValue, String falseValue);
}

// 2. MySQL实现
@Component("mysqlDialect")
public class MySqlDialect implements SqlDialect {
    @Override
    public String getPageSql(String sql, int offset, int size) {
        return sql + " LIMIT " + offset + ", " + size;
    }
    
    @Override
    public String getDateFormatFunction(String column, String format) {
        return "DATE_FORMAT(" + column + ", '" + format + "')";
    }
}

// 3. PostgreSQL实现
@Component("postgresDialect")
public class PostgresDialect implements SqlDialect {
    @Override
    public String getPageSql(String sql, int offset, int size) {
        return sql + " LIMIT " + size + " OFFSET " + offset;
    }
    
    @Override
    public String getDateFormatFunction(String column, String format) {
        return "TO_CHAR(" + column + ", '" + format + "')";
    }
}
```

**XML改造:**
```xml
<!-- MySQL版本 -->
<select id="selectPage" resultType="Order" databaseId="mysql">
    SELECT * FROM orders
    WHERE create_time > #{startTime}
    LIMIT #{offset}, #{size}
</select>

<!-- PostgreSQL版本 -->
<select id="selectPage" resultType="Order" databaseId="postgres">
    SELECT * FROM orders
    WHERE create_time > #{startTime}
    LIMIT #{size} OFFSET #{offset}
</select>
```

---

# 五、信创达梦数据库改造

## 5.1 信创改造背景

**什么是信创?**
- 信创 = 信息技术应用创新
- 国产化替代:操作系统、数据库、中间件、芯片等
- 常见信创数据库:达梦(DM)、人大金仓(KingBase)、神通(Oscar)

**为什么要改造?**
- 政策要求:政府、国企、金融等行业必须使用国产数据库
- 数据安全:核心数据不依赖国外技术

## 5.2 MySQL迁移到达梦数据库

### 5.2.1 达梦数据库特点

| 对比项 | MySQL | 达梦DM8 |
|--------|-------|---------|
| **兼容性** | - | 兼容Oracle、MySQL语法 |
| **驱动** | com.mysql.cj.jdbc.Driver | dm.jdbc.driver.DmDriver |
| **URL** | jdbc:mysql://host:3306/db | jdbc:dm://host:5236/db |
| **默认端口** | 3306 | 5236 |
| **字符串拼接** | CONCAT() | \|\| 或 CONCAT() |
| **分页** | LIMIT | LIMIT 或 ROWNUM |

### 5.2.2 项目改造步骤

**Step 1:添加达梦驱动**
```xml
<dependency>
    <groupId>com.dameng</groupId>
    <artifactId>DmJdbcDriver18</artifactId>
    <version>8.1.2.192</version>
</dependency>
```

**Step 2:配置文件改造**
```yaml
# application-dm.yml(达梦环境配置)
spring:
  datasource:
    driver-class-name: dm.jdbc.driver.DmDriver
    url: jdbc:dm://192.168.1.100:5236/SYSDBA
    username: SYSDBA
    password: SYSDBA001
    
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

mybatis-plus:
  configuration:
    # 达梦数据库标识
    database-id: dm
  global-config:
    db-config:
      # 达梦支持的主键策略
      id-type: auto
      # 表名大写(达梦默认大写)
      table-prefix: ""
      # 字段映射策略
      capital-mode: true
```

**Step 3:MyBatis-Plus配置**
```java
@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 分页插件(指定达梦数据库)
        PaginationInnerInterceptor paginationInterceptor = 
            new PaginationInnerInterceptor(DbType.DM);
        interceptor.addInnerInterceptor(paginationInterceptor);
        
        return interceptor;
    }
    
    /**
     * 数据库类型识别
     */
    @Bean
    public DatabaseIdProvider databaseIdProvider() {
        DatabaseIdProvider provider = new VendorDatabaseIdProvider();
        Properties properties = new Properties();
        properties.setProperty("MySQL", "mysql");
        properties.setProperty("DM DBMS", "dm");      // 达梦标识
        properties.setProperty("Oracle", "oracle");
        provider.setProperties(properties);
        return provider;
    }
}
```

### 5.2.3 SQL语句改造

**问题1:表名和字段名大小写**
```sql
-- MySQL:不区分大小写
SELECT * FROM orders WHERE order_no = 'xxx';

-- 达梦:默认大写,需要统一
SELECT * FROM ORDERS WHERE ORDER_NO = 'xxx';

-- 或者使用引号保持原样
SELECT * FROM "orders" WHERE "order_no" = 'xxx';
```

**解决方案:**
```java
// 方式1:全局配置大小写(推荐)
mybatis-plus:
  global-config:
    db-config:
      # 表名不转大写
      table-underline: true
      # 字段名不转大写
      column-underline: true

// 方式2:注解指定
@TableName(value = "orders", resultMap = "BaseResultMap")
public class Order {
    @TableField("order_no")
    private String orderNo;
}
```

**问题2:分页语法差异**
```xml
<!-- MySQL版本 -->
<select id="selectPage" resultType="Order" databaseId="mysql">
    SELECT * FROM orders
    LIMIT #{offset}, #{size}
</select>

<!-- 达梦版本(兼容MySQL语法)-->
<select id="selectPage" resultType="Order" databaseId="dm">
    SELECT * FROM orders
    LIMIT #{offset}, #{size}
</select>

<!-- 达梦原生分页(使用ROWNUM,性能更好)-->
<select id="selectPageNative" resultType="Order" databaseId="dm">
    SELECT * FROM (
        SELECT A.*, ROWNUM RN FROM (
            SELECT * FROM orders ORDER BY create_time DESC
        ) A WHERE ROWNUM <= #{endRow}
    ) WHERE RN > #{startRow}
</select>
```

**问题3:函数差异**
```xml
<!-- 通用写法(多数据库兼容)-->
<select id="selectByDateRange">
    SELECT * FROM orders
    WHERE 1=1
    <if test="startTime != null">
        <choose>
            <when test="_databaseId == 'mysql'">
                AND DATE_FORMAT(create_time, '%Y-%m-%d') >= #{startTime}
            </when>
            <when test="_databaseId == 'dm'">
                AND TO_CHAR(create_time, 'YYYY-MM-DD') >= #{startTime}
            </when>
        </choose>
    </if>
</select>
```

### 5.2.4 数据迁移工具

```java
/**
 * 数据迁移工具
 * 
 * 场景:MySQL数据迁移到达梦
 */
@Component
@Slf4j
public class DataMigrationService {
    
    @Autowired
    @Qualifier("mysqlJdbcTemplate")
    private JdbcTemplate mysqlJdbcTemplate;
    
    @Autowired
    @Qualifier("dmJdbcTemplate")
    private JdbcTemplate dmJdbcTemplate;
    
    /**
     * 迁移订单表数据
     */
    public void migrateOrders() {
        String selectSql = "SELECT * FROM orders";
        String insertSql = "INSERT INTO ORDERS (ID, ORDER_NO, USER_ID, TOTAL_AMOUNT, STATUS, CREATE_TIME) " +
                          "VALUES (?, ?, ?, ?, ?, ?)";
        
        // 分批迁移
        int batchSize = 1000;
        int offset = 0;
        int totalCount = 0;
        
        while (true) {
            // 查询MySQL数据
            List<Map<String, Object>> rows = mysqlJdbcTemplate.queryForList(
                selectSql + " LIMIT " + offset + ", " + batchSize
            );
            
            if (rows.isEmpty()) {
                break;
            }
            
            // 批量插入达梦
            dmJdbcTemplate.batchUpdate(insertSql, new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    Map<String, Object> row = rows.get(i);
                    ps.setLong(1, (Long) row.get("id"));
                    ps.setString(2, (String) row.get("order_no"));
                    ps.setLong(3, (Long) row.get("user_id"));
                    ps.setBigDecimal(4, (BigDecimal) row.get("total_amount"));
                    ps.setInt(5, (Integer) row.get("status"));
                    ps.setTimestamp(6, (Timestamp) row.get("create_time"));
                }
                
                @Override
                public int getBatchSize() {
                    return rows.size();
                }
            });
            
            totalCount += rows.size();
            offset += batchSize;
            
            log.info("已迁移 {} 条数据", totalCount);
        }
        
        log.info("迁移完成,共 {} 条数据", totalCount);
    }
    
    /**
     * 数据校验
     */
    public boolean validateData(String tableName) {
        // 1. 校验数量
        Long mysqlCount = mysqlJdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM " + tableName, Long.class);
        Long dmCount = dmJdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM " + tableName.toUpperCase(), Long.class);
        
        if (!mysqlCount.equals(dmCount)) {
            log.error("数据量不一致: MySQL={}, DM={}", mysqlCount, dmCount);
            return false;
        }
        
        // 2. 抽样校验数据一致性
        List<Map<String, Object>> mysqlSample = mysqlJdbcTemplate.queryForList(
            "SELECT * FROM " + tableName + " ORDER BY id LIMIT 100");
        List<Map<String, Object>> dmSample = dmJdbcTemplate.queryForList(
            "SELECT * FROM " + tableName.toUpperCase() + " ORDER BY ID FETCH FIRST 100 ROWS ONLY");
        
        // 比对数据...
        
        return true;
    }
}
```

### 5.2.5 兼容性配置

```java
/**
 * 多数据库兼容配置
 * 
 * 支持一套代码,同时兼容MySQL和达梦
 */
@Configuration
public class MultiDatabaseConfig {
    
    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;
    
    @Bean
    public DbType getDbType() {
        if (driverClassName.contains("mysql")) {
            return DbType.MYSQL;
        } else if (driverClassName.contains("dm")) {
            return DbType.DM;
        } else if (driverClassName.contains("oracle")) {
            return DbType.ORACLE;
        }
        throw new RuntimeException("不支持的数据库类型");
    }
    
    @Bean
    public SqlDialect sqlDialect() {
        DbType dbType = getDbType();
        switch (dbType) {
            case MYSQL:
                return new MySqlDialect();
            case DM:
                return new DmDialect();
            default:
                throw new RuntimeException("不支持的数据库类型");
        }
    }
}

/**
 * 达梦SQL方言
 */
public class DmDialect implements SqlDialect {
    
    @Override
    public String getLimitSql(String sql, int offset, int size) {
        // 达梦8支持MySQL的LIMIT语法
        return sql + " LIMIT " + offset + ", " + size;
    }
    
    @Override
    public String getDateFormatSql(String column, String format) {
        // 达梦使用TO_CHAR
        return "TO_CHAR(" + column + ", '" + convertFormat(format) + "')";
    }
    
    private String convertFormat(String mysqlFormat) {
        // MySQL: %Y-%m-%d → 达梦: YYYY-MM-DD
        return mysqlFormat
            .replace("%Y", "YYYY")
            .replace("%m", "MM")
            .replace("%d", "DD")
            .replace("%H", "HH24")
            .replace("%i", "MI")
            .replace("%s", "SS");
    }
}
```

## 5.3 信创改造总结

**改造清单:**
```
✅ 驱动更换:MySQL → 达梦驱动
✅ URL修改:jdbc:mysql → jdbc:dm
✅ 表名大小写:统一处理
✅ SQL语法:适配达梦语法
✅ 分页逻辑:LIMIT → ROWNUM
✅ 函数替换:DATE_FORMAT → TO_CHAR
✅ 数据迁移:MySQL数据 → 达梦数据
✅ 测试验证:功能测试、性能测试
```

**注意事项:**
1. **提前测试**:在测试环境充分测试
2. **灰度发布**:先部分业务,再全量
3. **数据校验**:迁移后务必校验数据一致性
4. **性能对比**:对比MySQL和达梦的性能差异
5. **回滚方案**:准备快速回滚到MySQL的方案

---

# 六、面试常见问题

## Q1:MyBatis-Plus的分页查询原理?

**答**:
MyBatis-Plus通过拦截器实现分页:

1. **PaginationInnerInterceptor** 拦截SQL
2. 解析SQL,获取总记录数(COUNT查询)
3. 改写SQL,添加分页语句(LIMIT)
4. 执行分页查询
5. 封装结果到Page对象

```java
// 原始SQL
SELECT * FROM orders WHERE status = 1

// 自动改写为
// 1. 查总数
SELECT COUNT(*) FROM orders WHERE status = 1
// 2. 分页查询
SELECT * FROM orders WHERE status = 1 LIMIT 0, 10
```

---

## Q2:分库分表后如何保证数据一致性?

**答**:

1. **单表事务**:操作同一张表,本地事务即可

2. **跨表事务**:使用分布式事务(Seata)

3. **最终一致性**:
   - 本地消息表
   - MQ保证
   - 定时补偿

4. **避免跨库JOIN**:
   - 数据冗余
   - 应用层聚合
   - 宽表设计

---

## Q3:信创改造遇到的最大困难是什么?

**答**:

**最大困难:SQL兼容性**

我们项目有上千条SQL,改造时遇到:

1. **函数差异**:DATE_FORMAT、IF、CONCAT等
2. **分页差异**:LIMIT → ROWNUM
3. **大小写敏感**:达梦默认大写

**解决方案:**
1. 抽象SQL方言接口
2. XML中用databaseId区分
3. 编写自动化测试,对比MySQL和达梦结果
4. 建立SQL兼容性文档

---

## 📝 面试速记卡

```
【MyBatis-Plus高阶】
LambdaQueryWrapper - 类型安全,支持重构
自定义SQL + MP分页 - IPage<VO> + ${ew.customSqlSegment}
批量操作 - saveBatch(1000条一批)
自动填充 - MetaObjectHandler(createTime/updateTime)
逻辑删除 - @TableLogic
多租户 - TenantLineInnerInterceptor

【分库分表】
时机:单表 > 1000万
方案:ShardingSphere(推荐)、MyCAT
策略:按月分表、按用户Hash
一致性:Seata、本地消息表

【信创改造】
MySQL → 达梦DM8
驱动:dm.jdbc.driver.DmDriver
URL:jdbc:dm://host:5236/db
难点:SQL兼容性、大小写敏感
方案:databaseId、SQL方言、自动化测试
```

---

**文档版本**:v1.0  
**创建时间**:2026-01-19  
**适用场景**:MyBatis-Plus进阶、分库分表、信创改造面试


评论