维基框架(Wiki-Framework) 1.2.0:Mybatis 升级为主从读写分离

先说结论

1.2.0 之前,只是对单库 MyBatis + Druid 的封装。业务要现读写分离,得引入wiki-datasource-jap模块来实现读写分析或者多数据源。

这次改动的核心的地方:

@Master / @Slave 标注 Mapper 方法(类上),框架自动切换(通过AOP切面拦截)主从;没标注的默认走主库。

顺便把配置绑不上、Mapper 扫不到、@RefreshScope 早期访问报错这些BUG也修了。


一、新增了什么?

1. 注解:怎走主库、怎走从库

| 注解 | 作用 | | --- | --- | | @Master | 强制走主库(方法或类上均可) | | @Slave | 强制走从库(方法或类上均可) |

插入、修改等写操作或者强一致性读,则标 @Master(或不标,默认就是主库)。查询为主、能接受延迟的,则标 @Slave。 以下是示例:

bash 复制代码
import com.cdkjframework.datasource.mybatis.annotation.Master;
import com.cdkjframework.datasource.mybatis.annotation.Slave;

/**
 * 默认主库:可不加注解也行
 */
public interface RmsUserMapper {
    RmsUser selectById(Long id);
    int updateById(RmsUser user);
}

/**
 *  只读查询走从库 
 */
public interface RmsUserQueryMapper {
    @Slave
    List<RmsUser> selectList(RmsUser query);
}

/**
 * 类上整体走从库,个别写操作再强制回主库
 */
@Slave
public interface RmsUserReportMapper {

    List<RmsUser> selectReport();

    @Master
    int refreshSnapshot();
}

2. 主要枚举、上下文与切面拦截(核心)

| 类 | 作用 | | --- | --- | | DataSourceType | 枚举:MASTER / SLAVE | | DataSourceContextHolder | ThreadLocal,存当前线程该用哪个库 | | DynamicDataSource | 继承 AbstractRoutingDataSource,真正做路由 | | DataSourceAspect | AOP 拦截 *..mapper..*,切库并打日志 |

要实现切换库规则很简单:

bash 复制代码
// DataSourceAspect 核心逻辑(示意)
DataSourceType dataSourceType;
if (hasMasterAnnotation(method, targetClass, targetMethod)) {
  dataSourceType = DataSourceType.MASTER;
} else if (hasSlaveAnnotation(method, targetClass, targetMethod)) {
  dataSourceType = DataSourceType.SLAVE;
} else {
  // 未标注 @Master / @Slave 时默认走主库
  dataSourceType = DataSourceType.MASTER;
}

每次切换都会输出 INFO 日志,排查「到底连接的是那个库」时很有用:

例如:

bash 复制代码
数据源切换: MASTER -> com.framewiki.example.mapper.RmsUserMapper.updateById()
数据源切换: SLAVE  -> com.framewiki.example.mapper.RmsUserQueryMapper.selectList()

3. 主从 Mapper 单独扫描

| 类 | 作用 | | --- | --- | | MapperPrimaryScannerConfiguration | 扫主库 Mapper 包,绑 primarySessionFactory | | MapperScannerConfiguration | 扫从库 Mapper 包,绑 sessionFactory | | MybatisMapperScanSupport | 从 Environment 用 Binder 解析配置,避免启动早期读不到属性问题 |

4. 测试

补了 DataSourceAspectTestMybatisConfigBindingTestMapperPrimaryScannerConfigurationTestMapperScannerConfigurationTest,涵盖了注解组合、配置绑定、扫描跳过/回退等场景的测试。


二、优化了什么?

1. 优化了配置读不到 / 重复 Bean 问题

因为以前 MybatisConfig 同时挂了 @Configuration@EnableConfigurationProperties。业务启动类如果再宽泛扫描 com.cdkjframework,容易注册出两个 Bean:一个绑定配置,一个则没有绑定配置。注入时都不知道拿到的是哪个。

现在 :去掉了多余的 @Configuration,只走 @EnableConfigurationProperties

2. Mapper 扫描时序

BeanDefinitionRegistryPostProcessor 执行得就很早了,原来是直接注入 MybatisConfig 并调用 getMaster()

  • 属性往往还没绑定完成

  • 再碰上 @RefreshScope,可能直接 No Scope registered for scope name 'refresh'

现在 :扫描类实现 EnvironmentAware,通过 Binder 读 spring.datasource,不依赖已创建完成的配置 Bean,就避免了配置绑定过早的问题。

bash 复制代码
// MybatisMapperScanSupport(示意)
static MybatisConfig bindMybatisConfig(Environment environment) {
    return Binder.get(environment)
        .bind("spring.datasource", Bindable.of(MybatisConfig.class))
        .orElseGet(MybatisConfig::new);
}

3. 默认直接走主库

现在 :只会读取 @Master / @Slave 注解,没注解的一律走主库。

4. 使用会话工厂更稳定

  • primarySessionFactory 标注 @Primary

  • sqlSessionTemplate 明确 @Qualifier("primarySessionFactory")

  • 初始化失败直接抛异常,避免悄悄返回 null


三、调整了什么?

1. 配置结构调整:支持 master / slave

MybatisConfig 从扁平的单库支持,修改成可嵌套的主从支持。公共 Mapper 包和 XML 仍可写在顶层,主从共用。

配置示例:

bash 复制代码
spring:
  datasource:
    # 公共扫描包 / XML(主从均可回退到这里)
    mybatis-mapper:
      - com.framewiki.example.mapper
    mybatis-mapper-xml:
      - /mybatis/**/*.xml

    # Druid 连接池(DataSourceConfig 读取,建议配上)
    max-active: 20
    initial-size: 5
    min-idle: 5
    filters: stat,wall
    # 主库配置
    master:
      url: jdbc:mysql://10.10.0.33:3306/mobile?characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

    # 从库配置
    slave:
      url: jdbc:mysql://10.10.0.33:3306/mobile?characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

如果只配了 master、没配 slave,从库会回退到主库连接地址,就会以单库场景跑。

2. 双数据源 + 双 SqlSessionFactory

| 调整前 | 调整后 | | --- | --- | | 一个 Druid 数据源 | masterDataSource + slaveDataSource + dynamicDataSource | | 一个 SqlSessionFactory | primarySessionFactory(主)+ sessionFactory(从) |

3. 自动配置装配方式

MybatisAutoConfiguration 里,Mapper 扫描从 @Import 改为静态 @Bean 注册方式便于配合 EnvironmentAware

4. MySQL 驱动调整为 test scope

pom.xmlmysql-connector-j 只在测试依赖中引入,避免把 GPL 协议带进业务发行中。

实际业务项目必须自己引入驱动,否则会出现:

bash 复制代码
ClassNotFoundException: com.mysql.cj.jdbc.Driver

需要实际业务项目自行引入驱动:

bash 复制代码
<!-- 业务项目 pom.xml -->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
</dependency>

四、怎么接入(完整示例)?

1. 依赖

bash 复制代码
<dependency>
  <groupId>com.framewiki</groupId>
  <artifactId>wiki-datasource</artifactId>
  <version>1.2.0</version>
</dependency>

<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
</dependency>

2. 启动类

bash 复制代码
import com.cdkjframework.datasource.mybatis.annotation.EnableAutoMybatis;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;

@EnableAutoMybatis
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

若使用 wiki-all@EnableAutoWiki,内部已包含了 @EnableAutoMybatis,则不必再单独添加。

3. Mapper 与 Service

bash 复制代码
// mapper/RmsUserMapper.java ------ 写操作,默认走主库
public interface RmsUserMapper {
    int modifyRmsUser(RmsUserEntity entity);
    RmsUserEntity selectById(Long id);
}

// mapper/RmsUserQueryMapper.java ------ 列表查询走从库
import com.cdkjframework.datasource.mybatis.annotation.Slave;

public interface RmsUserQueryMapper {
    @Slave
    List<RmsUserEntity> selectList(RmsUserEntity query);
}
bash 复制代码
/**
 * 服务实现类
 */
@Service
@RequiredArgsConstructor
public class RmsUserServiceImpl implements RmsUserService {

    /**
     * 写操作走主库
     */
    private final RmsUserMapper rmsUserMapper;

    /**
     * 查询操作走从库
     */
    private final RmsUserQueryMapper rmsUserQueryMapper;

    /**
     * 写操作走主库
     */
    @Transactional(rollbackFor = Exception.class)
    public void modifyRmsUser(RmsUserEntity entity) {
        // 走主库
        rmsUserMapper.modifyRmsUser(entity);
    }

    /**
     * 查询操作走从库
     */
    public List<RmsUserEntity> listUsers(RmsUserEntity query) {
        // 走从库(方法上有 @Slave)
        return rmsUserQueryMapper.selectList(query);
    }
}

4. 怎么确认切换正确的主从库?

启动后调一次写、一次读,日志里应能看到正确的切换日志:

bash 复制代码
数据源切换: MASTER -> ...RmsUserMapper.modifyRmsUser()
数据源切换: SLAVE  -> ...RmsUserQueryMapper.selectList()

如果日志太多时,可单独调整级别:

bash 复制代码
logging:
  level:
    com.cdkjframework.datasource.mybatis.aspect.DataSourceAspect: INFO

五、小结

wiki-datasource 在 1.2.0 的变化可以概括为以下方面:

  1. 新增@Master / @Slave 注解、主从扫描、切换日志输出

  2. 优化:配置绑定、扫描时序、默认主库规则、会话工厂稳定性

  3. 调整:YAML 支持 master/slave、双数据源双工厂、驱动下沉业务方

  4. 接入@EnableAutoMybatis + 配置主从注解 + Mapper 按需标注注解 + 自行引入 MySQL 驱动

需要读写分离需求时,优先用注解标清楚;没有从库时,默认主库也能正常工作。

相关推荐
Memory_荒年1 小时前
Java函数式接口:把代码变成“拼乐高”的快乐,你体验过吗?
java
Memory_荒年1 小时前
WebSocket:让服务器学会“主动搭讪”的黑科技
java
Larcher1 小时前
MCP协议实战:让AI Agent跨进程调用工具的正确姿势
人工智能·后端
在水一缸2 小时前
深度解析 GPT-5.6:大模型架构演进与复杂任务实战指南
大数据·人工智能·gpt·架构·大模型·架构演进·gpt-5.6
Henrii_历小海2 小时前
WhatsApp Business API 2026 计费架构重构:从“对话计费“到“按消息计费“的技术实现与工程应对
java·重构·架构
炸薯条!2 小时前
从零开始学C++ (内存管理)
java·jvm·c++
Reart2 小时前
Leetcode 121. 买卖股票的最佳时机(717)
后端·算法
许彰午2 小时前
100_Python面试常见问题汇总
java·python·面试
zandy10112 小时前
多租户SaaS架构下的BI数据隔离与权限治理体系
数据库·架构·数据加密