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 2018-07-26 14:05 mumuxinfei 阅读(2562) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .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)--时间序列检索和面检索的应用场景实战