spring+mybatis的多源数据库配置实战

 

前言:
  关于spring+mybatis的多源数据库配置, 其实是个老生常谈的事情. 网上的方案出奇的一致, 都是借助AbstractRoutingDataSource进行动态数据源的切换.
  这边再无耻地做一回大自然的搬运工, 除了做下笔记, 更多的希望是作为一个切入点, 能探寻下mybatis实现分库分表的解决方案.

 

基本原理:
  关于mybatis的配置, 基本遵循如下的概念流:

DB(数据库对接信息)->数据源(数据库连接池配置)->session工厂(连接管理与数据访问映射关联)->DAO(业务访问封装).

  对于定义的sqlmapper接口类, mybatis会为这些类动态生成一个代理类, 隐藏了连接管理(获取/释放), 参数设置/SQL执行/结果集映射等细节, 大大简化了开发工作.
  而连接管理涉及到具体的DataSource类实现机制, 在具体执行sql前, 其DB源的选定还有操作空间. 这也为DB路由(切换)提供了口子, 而AbstractRoutingDataSource的引入, 一定程度上为DB自由切换提供了便利.

 

配置工作:
  先编写jdbc.properties的内容:

1
2
3
4
5
6
7
8
9
10
11
# db1的配置
db1.jdbc.url=jdbc:mysql://127.0.0.1:3306/db_account_1?useUnicode=true&characterEncoding=utf-8
db1.jdbc.username=rd
db1.jdbc.password=rd
db1.jdbc.driver=com.mysql.jdbc.Driver
 
# db2的配置
db2.jdbc.url=jdbc:mysql://127.0.0.1:3306/db_account_2?useUnicode=true&characterEncoding=utf-8
db2.jdbc.username=rd
db2.jdbc.password=rd
db2.jdbc.driver=com.mysql.jdbc.Driver

  编辑mybatis-config.xml(对mybatis做一些基础通用配置)的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
    <settings>
        <!-- 全局映射器启用缓存 -->
        <setting name="cacheEnabled" value="true" />
        <!-- 查询时,关闭关联对象即时加载以提高性能 -->
        <setting name="lazyLoadingEnabled" value="true" />
        <!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指定),不会加载关联表的所有字段,以提高性能 -->
        <setting name="aggressiveLazyLoading" value="false" />
        <!-- 允许插入 NULL -->
        <setting name="jdbcTypeForNull" value="NULL" />
    </settings>
</configuration>

  编辑application-context.xml的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
 
    <aop:aspectj-autoproxy proxy-target-class="true"/>
 
    <context:component-scan base-package="com.springapp.mvc"/>
    <context:annotation-config />
 
    <!-- 加载jdbc.properties配置文件 -->
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:conf/jdbc.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="UTF-8"/>
        <property name="ignoreUnresolvablePlaceholders" value="true"/>
    </bean>
 
    <!-- 配置数据源1 -->
    <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${db1.jdbc.url}"/>
        <property name="username" value="${db1.jdbc.username}"/>
        <property name="password" value="${db1.jdbc.password}"/>
        <property name="driverClassName" value="${db1.jdbc.driver}" />
    </bean>
 
    <!-- 配置数据源2 -->
    <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${db2.jdbc.url}"/>
        <property name="username" value="${db2.jdbc.username}"/>
        <property name="password" value="${db2.jdbc.password}"/>
        <property name="driverClassName" value="${db2.jdbc.driver}" />
    </bean>
 
    <!-- 配置动态数据源 -->
    <bean id="dynamicDatasource" class="com.springapp.mvc.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map>
                <entry key="db1" value-ref="dataSource1"/>
                <entry key="db2" value-ref="dataSource2"/>
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="dataSource1"/>
    </bean>
 
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dynamicDatasource"/>
        <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
        <property name="mapperLocations">
            <list></list>
        </property>
    </bean>
 
    <!--mybatis的配置-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.springapp.mvc.dal"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
 
</beans>

  注: 这里面涉及一些类, 会在下文中定义.

 

依赖引入:
  这边使用了alibaba开源的druid作为数据库连接池.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>
 
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>
 
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
 
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.2</version>
</dependency>
 
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>

  

基础代码编写:
  主要是db路由的datasource实现类, 以及辅助的注解工具类.
  定义db来源的枚举类:

1
2
3
4
5
6
7
8
9
10
@Getter
@AllArgsConstructor
public enum DataSourceKey {
 
    DB1("db1"),
    DB2("db2");
 
    private String dbKey;
 
}

  定义标示当前激活db的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DatasourceContextHolder {
 
    private static final ThreadLocal<String> contextHolder
            = new ThreadLocal<String>();
 
    // 设置数据源
    public static void setDataSourceType(DataSourceKey dbKey) {
        contextHolder.set(dbKey.getDbKey());
    }
 
    // 获取当前的数据源
    public static String getDataSourceType() {
        return contextHolder.get();
    }
 
    // 清空数据源
    public static void clearDataSourceType() {
        contextHolder.remove();
    }
 
}

  注: 利用了ThreadLocal来保存当前选择的db源
  定义AbstractRoutingDataSource的实现类:

1
2
3
4
5
6
7
8
public class DynamicDataSource extends AbstractRoutingDataSource {
 
    @Override
    protected Object determineCurrentLookupKey() {
        return DatasourceContextHolder.getDataSourceType();
    }
 
}

  注: 只要重载determineCurrentLookupKey()函数即可.
  定义注解:

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSelector {
    DataSourceKey dataSource() default DataSourceKey.DB1;
}

  定义切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Aspect
@Component
@Order(1)
public class DataSourceSelectorAdvice {
 
    // 定义切点, 用于db源切换
    @Pointcut("@annotation(com.springapp.mvc.datasource.DataSourceSelector)")
    public void selectorDB() {
    }
 
    @Around("selectorDB() && @annotation(dataSourceSelector)")
    public Object aroundSelectDB(ProceedingJoinPoint pjp, DataSourceSelector dataSourceSelector) throws Throwable {
        // 设置具体的数据源
        DatasourceContextHolder.setDataSourceType(dataSourceSelector.dataSource());
        try {
            // 执行拦截的方法本体
            return pjp.proceed();
        } finally {
            // 清空设置的数据源
            DatasourceContextHolder.clearDataSourceType();
        }
    }
 
}

  这些代码构成了动态切换db源的主干框架.

 

业务代码编写:
  编写DO类:

1
2
3
4
5
6
7
8
9
10
@Getter
@Setter
@ToString
public class AccountDO {
 
    private String username;
 
    private String password;
 
}

  编写sqlmapper接口类:

1
2
3
4
5
6
7
8
9
10
11
12
@Repository
public interface AccountMapper {
 
    @Select("SELECT username, password FROM tb_account WHERE user_id = #{user_id}")
    @Results({
            @Result(property = "userId",   column = "user_id",  jdbcType = JdbcType.VARCHAR),
            @Result(property = "username", column = "username", jdbcType = JdbcType.VARCHAR),
            @Result(property = "password", column = "password", jdbcType = JdbcType.VARCHAR),
    })
    AccountDO queryByUserId(@Param("user_id") String userId);
 
}

  编写service类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class AccountService {
 
    @Resource
    private AccountMapper accountMapper;
 
    // *) 从db1获取数据
    @DataSourceSelector(dataSource = DataSourceKey.DB1)
    public AccountDO queryByUserId1(String userId) {
        return accountMapper.queryByUserId(userId);
    }
 
    // *) 从db2获取数据
    @DataSourceSelector(dataSource = DataSourceKey.DB2)
    public AccountDO queryByUserId2(String userId) {
        return accountMapper.queryByUserId(userId);
    }
 
}

  Aspectj对接口(interface)无效, 对具体的实体类才其作用, 因为sqlmapper接口类会被mybatis生成一个动态类, 因此需要加切面(db切换), 需要在service层去实现.

 

验证数据准备:
  本地创建了两个db, 都创建相同的表tb_account.

1
2
3
4
5
6
7
CREATE TABLE `tb_account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(32) NOT NULL,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`), UNIQUE KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  db1的tb_account有账号数据(1001, lilei).
  db2的tb_account有账号数据(2001, hanmeimei).
  

 

测试:
  编写单测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:application-context.xml"})
public class AccountServiceTest {
 
    @Resource
    private AccountService accountService;
 
    @Test
    public void queryByUserId1() {
        // 用户id:1001, 落在db1中, 不在db2
        String userId = "1001";
 
        AccountDO accountDO1 = accountService.queryByUserId1(userId);
        Assert.assertNotNull(accountDO1);       // 存在断言
 
        AccountDO accountDO2 = accountService.queryByUserId2(userId);
        Assert.assertNull(accountDO2);          // 不存在断言
    }
 
    @Test
    public void queryByUserId2() {
        // 用户id:2001, 不在db1中, 在db2中
        String userId = "2001";
 
        AccountDO accountDO1 = accountService.queryByUserId1(userId);
        Assert.assertNull(accountDO1);          // 不存在断言
 
        AccountDO accountDO2 = accountService.queryByUserId2(userId);
        Assert.assertNotNull(accountDO2);       // 存在断言
    }
 
}

  运行的结果符合预期.

 

后记:
  对于微服务的盛行, 其实多源的数据源(基于业务划分)基本就不存在, 如果存在, 要么业务刚发展起来, 要么就是公司的基础设施太薄弱了^_^. 网上也看到有人用来主从(master/slave)的配置, 其实对于有一定规模的公司而言, mysql的主从分离都由类db proxy的中间件服务承包了.
  那他的意义究竟在哪呢? 其实我感觉还是给mysql的分库分表, 提供了一种可行的思路.

 

posted on   mumuxinfei  阅读(2562)  评论(0编辑  收藏  举报

编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2014-07-26 HBase 实战(2)--时间序列检索和面检索的应用场景实战

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示