zoukankan      html  css  js  c++  java
  • mybatis: 利用多数据源实现分库存储

    之前写过一篇mybatis 使用经验小结 提到过多数据源的处理方式,虽然简单但是姿势不太优雅,今天介绍一些更美观的办法:

    spring中有一个AbstractRoutingDataSource的抽象类可以很好的支持多数据源,我们只需要继续它即可。

    package com.cnblogs.yjmyzz.utils;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    public class RoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
    
            return DBContext.getDBKey();
        }
    }
    

    很简单,就一个方法。其中DBContext的代码如下:

    package com.cnblogs.yjmyzz.utils;
    
    public class DBContext {
    
        //define count of database and it must match with resources/properties/jdbc.properties
        private static final int DB_COUNT = 2;
    
        private static final ThreadLocal<String> tlDbKey = new ThreadLocal<String>();
    
        public static String getDBKey() {
            return tlDbKey.get();
        }
    
        public static void setDBKey(String dbKey) {
            tlDbKey.set(dbKey);
        }
    
        public static String getDBKeyByUserId(int userId) {
            int dbIndex = userId % DB_COUNT;
            return "db_" + (++dbIndex);
        }
    }
    

    主要利用了ThreadLocal这个类在每个线程中保持自己私有的变量。

    这里我模拟了一个分库的场景:假设一个应用允许用户注册,但是用户数量太多,全都放在一个数据库里,记录过多,会导致数据库性能瓶颈,比较容易想到的办法,把用户的数据分散到多个数据库中保存(注:可能马上有同学会说了,分开存了,要查询所有用户怎么办?这确实是分库带来的一个弊端,但也有相应的解决方案,本文先不讨论这个,以免跑题)。

    假设我们有二个数据库,里面的表结构完全相同,有一张表T_USER用于保存用户数据,问题来了,如果有N个用户要注册,id分别是1、2、3...,服务端接到参数后,怎么知道把这些数据分别插入到这二个库中,必然要有一个规则 ,比较简单的办法就是取模,所以上面的getDBKeyByUserId就是干这个的。

    然后是jdbc的属性配置文件:

    jdbc-driver=com.mysql.jdbc.Driver
    
    jdbc-key-1=db_1
    jdbc-url-1=jdbc:mysql://default:3306/db_1?useUnicode=true&characterEncoding=utf8
    jdbc-user-1=test
    jdbc-password-1=123456
    
    jdbc-key-2=db_2
    jdbc-url-2=jdbc:mysql://default:3306/db_2?useUnicode=true&characterEncoding=utf8
    jdbc-user-2=test
    jdbc-password-2=123456
    

    接下来是spring的配置文件:

     1 <?xml version="1.0" encoding="UTF-8"?>
     2 <beans xmlns="http://www.springframework.org/schema/beans"
     3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     4        xmlns:context="http://www.springframework.org/schema/context"
     5        xsi:schemaLocation="http://www.springframework.org/schema/beans
     6        http://www.springframework.org/schema/beans/spring-beans.xsd
     7          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
     8 
     9 
    10     <context:annotation-config/>
    11 
    12     <context:component-scan base-package="com.cnblogs.yjmyzz"/>
    13 
    14     <bean id="propertiesFactoryBean"
    15           class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    16         <property name="locations">
    17             <list>
    18                 <value>classpath:properties/jdbc.properties</value>
    19             </list>
    20         </property>
    21     </bean>
    22 
    23     <context:property-placeholder properties-ref="propertiesFactoryBean" ignore-unresolvable="true"/>
    24 
    25     <bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
    26           destroy-method="close">
    27         <property name="driverClassName" value="${jdbc-driver}"/>
    28         <property name="url" value="${jdbc-url-1}"/>
    29         <property name="username" value="${jdbc-user-1}"/>
    30         <property name="password" value="${jdbc-password-1}"/>
    31         <property name="filters" value="stat"/>
    32         <property name="maxActive" value="20"/>
    33         <property name="initialSize" value="1"/>
    34         <property name="maxWait" value="60000"/>
    35         <property name="minIdle" value="1"/>
    36         <property name="timeBetweenEvictionRunsMillis" value="3000"/>
    37         <property name="minEvictableIdleTimeMillis" value="300000"/>
    38         <property name="validationQuery" value="SELECT 'x'"/>
    39         <property name="testWhileIdle" value="true"/>
    40         <property name="testOnBorrow" value="false"/>
    41         <property name="testOnReturn" value="false"/>
    42         <property name="poolPreparedStatements" value="true"/>
    43         <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
    44         <property name="connectionInitSqls" value="set names utf8mb4;"/>
    45     </bean>
    46 
    47     <bean id="dataSource1" parent="parentDataSource">
    48         <property name="url" value="${jdbc-url-1}"/>
    49         <property name="username" value="${jdbc-user-1}"/>
    50         <property name="password" value="${jdbc-password-1}"/>
    51     </bean>
    52 
    53     <bean id="dataSource2" parent="parentDataSource">
    54         <property name="url" value="${jdbc-url-2}"/>
    55         <property name="username" value="${jdbc-user-2}"/>
    56         <property name="password" value="${jdbc-password-2}"/>
    57     </bean>
    58 
    59     <!-- config switch routing db -->
    60     <bean id="dataSource" class="com.cnblogs.yjmyzz.utils.RoutingDataSource">
    61         <property name="targetDataSources">
    62             <map key-type="java.lang.String">
    63                 <entry key="${jdbc-key-1}" value-ref="dataSource1"/>
    64                 <entry key="${jdbc-key-2}" value-ref="dataSource2"/>
    65             </map>
    66         </property>
    67     </bean>
    68 
    69     <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    70         <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    71         <property name="dataSource" ref="dataSource"/>
    72         <property name="mapperLocations">
    73             <array>
    74                 <value>classpath:mybatis/*.xml</value>
    75             </array>
    76         </property>
    77     </bean>
    78 
    79     <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    80         <property name="basePackage" value="com.cnblogs.yjmyzz.mapper"/>
    81     </bean>
    82 
    83 </beans>
    View Code

    关键的是parentDataSource,dataSource1,dataSource2,dataSource这几个bean的配置,一看就懂。

    服务端的核心代码:

    package com.cnblogs.yjmyzz.service.impl;
    
    import com.cnblogs.yjmyzz.entity.UserEntity;
    import com.cnblogs.yjmyzz.mapper.UserEntityMapper;
    import com.cnblogs.yjmyzz.service.UserService;
    import com.cnblogs.yjmyzz.utils.DBContext;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * Created by yangjunming on 2/15/16.
     * author: yangjunming@huijiame.com
     */
    
    @Service("userService")
    public class UserServiceImpl implements UserService {
    
        @Autowired
        UserEntityMapper userEntityMapper;
    
    
        @Override
        public void addUser(UserEntity userEntity) {
            //switch db
            DBContext.setDBKey(DBContext.getDBKeyByUserId(userEntity.getUserId()));
            userEntityMapper.insertSelective(userEntity);
        }
    
        @Override
        public UserEntity getUser(int userId) {
            //switch db
            DBContext.setDBKey(DBContext.getDBKeyByUserId(userId));
            return userEntityMapper.selectByPrimaryKey(userId);
        }
    }
    

    注意:25,32行在调用mybatis操作数据库前,先根据需要切换到不同的数据库,然后再操作。 

    运行完成后,可以看下db_1,db_2这二个数据库,确认数据是否已经分散存储到每个库中:

    点击看大图  

    如果不喜欢在代码里手动切换db,也可以用注解的方式自动切换,比如:我们又增加了一个db_main

    jdbc-driver=com.mysql.jdbc.Driver
    
    jdbc-key-1=db_1
    jdbc-url-1=jdbc:mysql://default:3306/db_1?useUnicode=true&characterEncoding=utf8
    jdbc-user-1=test
    jdbc-password-1=123456
    
    jdbc-key-2=db_2
    jdbc-url-2=jdbc:mysql://default:3306/db_2?useUnicode=true&characterEncoding=utf8
    jdbc-user-2=test
    jdbc-password-2=123456
    
    jdbc-key-main=db_main
    jdbc-url-main=jdbc:mysql://default:3306/db_main?useUnicode=true&characterEncoding=utf8
    jdbc-user-main=test
    jdbc-password-main=123456
    

    然后在spring配置文件里,要做些调整:

     1     <bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
     2           destroy-method="close">
     3         <property name="driverClassName" value="${jdbc-driver}"/>
     4         <property name="url" value="${jdbc-url-1}"/>
     5         <property name="username" value="${jdbc-user-1}"/>
     6         <property name="password" value="${jdbc-password-1}"/>
     7         <property name="filters" value="stat"/>
     8         <property name="maxActive" value="20"/>
     9         <property name="initialSize" value="1"/>
    10         <property name="maxWait" value="60000"/>
    11         <property name="minIdle" value="1"/>
    12         <property name="timeBetweenEvictionRunsMillis" value="3000"/>
    13         <property name="minEvictableIdleTimeMillis" value="300000"/>
    14         <property name="validationQuery" value="SELECT 'x'"/>
    15         <property name="testWhileIdle" value="true"/>
    16         <property name="testOnBorrow" value="false"/>
    17         <property name="testOnReturn" value="false"/>
    18         <property name="poolPreparedStatements" value="true"/>
    19         <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
    20         <property name="connectionInitSqls" value="set names utf8mb4;"/>
    21     </bean>
    22 
    23     <bean id="dataSource1" parent="parentDataSource">
    24         <property name="url" value="${jdbc-url-1}"/>
    25         <property name="username" value="${jdbc-user-1}"/>
    26         <property name="password" value="${jdbc-password-1}"/>
    27     </bean>
    28 
    29     <bean id="dataSource2" parent="parentDataSource">
    30         <property name="url" value="${jdbc-url-2}"/>
    31         <property name="username" value="${jdbc-user-2}"/>
    32         <property name="password" value="${jdbc-password-2}"/>
    33     </bean>
    34 
    35     <bean id="dataSourceMain" parent="parentDataSource">
    36         <property name="url" value="${jdbc-url-main}"/>
    37         <property name="username" value="${jdbc-user-main}"/>
    38         <property name="password" value="${jdbc-password-main}"/>
    39     </bean>
    40 
    41     <!-- method 1:  config switch routing db -->
    42     <bean id="dataSource" class="com.cnblogs.yjmyzz.utils.RoutingDataSource">
    43         <property name="targetDataSources">
    44             <map key-type="java.lang.String">
    45                 <entry key="${jdbc-key-1}" value-ref="dataSource1"/>
    46                 <entry key="${jdbc-key-2}" value-ref="dataSource2"/>
    47                 <entry key="${jdbc-key-main}" value-ref="dataSourceMain"/>
    48             </map>
    49         </property>
    50     </bean>
    51 
    52     <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    53         <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    54         <property name="dataSource" ref="dataSource"/>
    55         <property name="mapperLocations">
    56             <array>
    57                 <value>classpath:mybatis/*.xml</value>
    58             </array>
    59         </property>
    60     </bean>
    61 
    62     <bean id="userScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    63         <property name="basePackage" value="com.cnblogs.yjmyzz.mapper.user"/>
    64         <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    65     </bean>
    66 
    67     <!-- method 2: config annotation auto switch-->
    68     <bean id="sqlSessionFactoryMain" class="org.mybatis.spring.SqlSessionFactoryBean">
    69         <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    70         <property name="dataSource" ref="dataSourceMain"/>
    71         <property name="mapperLocations">
    72             <array>
    73                 <value>classpath:mybatis/*.xml</value>
    74             </array>
    75         </property>
    76     </bean>
    77 
    78     <bean id="orderScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    79         <property name="basePackage" value="com.cnblogs.yjmyzz.mapper.order"/>
    80         <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryMain"/>
    81     </bean>

    注意:67-81行,主要是增加了一个单独的sqlSessionFactoryMain,然后将一个新的MapperScannerConfigurer关联到它。

    新库里对应表的Mapper类可以这么写:

    @Resource(name = "orderScannerConfigurer")
    public interface OrderEntityMapper extends Mapper<OrderEntity> {
    }
    

    注解里name对应的值,必须与刚才spring文件里新增的MapperScannerConfigurer对应。

    这样,服务层就可以省去手动切换的代码了,即:

    public class UserServiceImpl implements UserService {
    
        @Autowired
        UserEntityMapper userEntityMapper;
    
        @Autowired
        OrderEntityMapper orderEntityMapper;
    
    
        @Override
        public void addUser(UserEntity userEntity) {
            //switch db
            DBContext.setDBKey(DBContext.getDBKeyByUserId(userEntity.getUserId()));
            userEntityMapper.insertSelective(userEntity);
        }
    
        @Override
        public UserEntity getUser(int userId) {
            //switch db
            DBContext.setDBKey(DBContext.getDBKeyByUserId(userId));
            return userEntityMapper.selectByPrimaryKey(userId);
        }
    
        @Override
        public void addOrder(OrderEntity orderEntity) {
            //since orderEntityMapper can auto switch db by annotation
            //so we don't need to switch db manually
            orderEntityMapper.insertSelective(orderEntity);
        }
    
        @Override
        public OrderEntity getOrder(int orderId) {
            //since orderEntityMapper can auto switch db by annotation
            //so we don't need to switch db manually
            return orderEntityMapper.selectByPrimaryKey(orderId);
        }
    
    }
    

    上述二种方式可以共存在同一个项目中,个人建议:如果分库的表结构相同,且表数量较多,第1种手动切换的方式比较适合,这样mapper类不用重复建多个,如果分库的表结构完全不同,第2种比较合适,因为表结构不同,mapper肯定也不同,所以mapper多个是无法避免的,这时候就宁可加点配置,代码中就不用手动切换,可以省事点。

    最后,在github上放了一份示例代码,供有需要的同学下载。

  • 相关阅读:
    4.JDBC技术
    3.MySQL数据库
    2.Oracle数据库
    (转载)Linux 套接字编程中的 5 个隐患
    一个Sqrt函数引发的血案
    二叉搜索树BinarySearchTree(C实现)
    vector,list.queue,array.....
    boost::Circular Buffer
    boost::operators
    Disjoint Sets
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/mybatis-with-multi-datasource.html
Copyright © 2011-2022 走看看