1. 程式人生 > >java自定義註解、mybatis 外掛 實現資料庫 分庫分表

java自定義註解、mybatis 外掛 實現資料庫 分庫分表

一、自定義註解實現分庫

為什麼會有資料庫切庫一說

首先,許多專案都有主庫與從庫,有的主庫後面甚至會有很多個從庫,主從庫之間的通常同步也很快,這為資料庫切庫提供了一個基礎,因為可以去不同的資料庫查詢,得到相同的結果(如果不同的資料庫是完全不同的,這個不在我們這篇文章討論的範圍之內,那個屬於讓專案支援多個數據源)

其次,隨著專案越來越大、操作的使用者越來越多,對資料庫的請求操作越來越多,很容易想到的是將讀寫請求分開,將寫請求交給主庫處理,讀請求直接從某個從庫讀取。這樣可以極大的減少大量對主庫的請求,提升主庫的效能。

接下來具體說一下如何通過自定義註解完成切庫(程式碼使用springboot實現):

第一步、定義我們自己的切庫註解類

自定義註解有幾點需要注意:

1)@Target 是作用的目標,介面、方法、類、欄位、包等等,具體看:ElementType

2)@Retention 是註解存在的範圍,RUNTIME代表的是註解會在class位元組碼檔案中存在,在執行時可以通過反射獲取到,具體看:RetentionPolicy

3)允許的變數,通常都要給定預設值,比如我們使用一個service時,可以@Service,也可以@Service("xxxx")

@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.METHOD
})
public @interface RoutingDataSource {

    String value() default DataSources.MASTER_DB;
}

第二步、定義需要使用的資料庫及配置

1、資料庫配置:application.properties,這裡要注意不同db的字首區別

## datasource master #
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/master?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=466420182

## datasource slave #
spring.datasourceslave.type=com.alibaba.druid.pool.DruidDataSource
spring.datasourceslave.driver-class-name=com.mysql.jdbc.Driver
spring.datasourceslave.url=jdbc:mysql://localhost:3306/slave?characterEncoding=UTF-8
spring.datasourceslave.username=root
spring.datasourceslave.password=466420182

2、定義支援的資料來源id:

public interface DataSources {

    String MASTER_DB = "masterDB";

    String SLAVE_DB = "slaveDB";
}

3、定義資料庫實體類並配置為多資料來源的形式

這裡不要忽略了通過 MapperScan 指定需要掃描的mybatis的介面類

@Configuration
public class DatasourceConfig {
    //destroy-method="close"的作用是當資料庫連線不使用的時候,就把該連線重新放到資料池中,方便下次使用呼叫.
    @Bean(destroyMethod =  "close", name = DataSources.MASTER_DB)
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }

    @Bean(destroyMethod =  "close", name = DataSources.SLAVE_DB)
    @ConfigurationProperties(prefix = "spring.datasourceslave")
    public DataSource dataSourceSlave() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }
}

4、配置成動態資料來源:

@Configuration
public class MybatisConfig {

    @Autowired
    @Qualifier(Datasources.MASTER_DB)
    private DataSource masterDB;

    @Autowired
    @Qualifier(DataSources.SLAVE_DB)
    private DataSource slaveDB;

    /**
     * 動態資料來源
     */
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 預設資料來源
        dynamicDataSource.setDefaultTargetDataSource(masterDB);

        // 配置多資料來源
        Map<Object, Object> dsMap = Maps.newHashMap();
        dsMap.put(DataSources.MASTER_DB, masterDB);
        dsMap.put(DataSources.SLAVE_DB, slaveDB);
        dynamicDataSource.setTargetDataSources(dsMap);

        return dynamicDataSource;
    }

    @Bean
    @ConfigurationProperties(prefix = "mybatis")
    public SqlSessionFactoryBean sqlSessionFactoryBean() {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 配置資料來源,此處配置為關鍵配置,如果沒有將 dynamicDataSource 作為資料來源則不能實現切換
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        return sqlSessionFactoryBean;
    }
}

第三步、使用ThreadLocal安全的管理當前程序使用的資料來源連線

@Slf4j
public class DataSourceContextHolder {

    /**
     * 預設資料來源
     */
    public static final String DEFAULT_DATASOURCE = DataSources.MASTER_DB;

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    // 設定資料來源名
    public static void setDB(String dbType) {
        log.debug("切換到{}資料來源", dbType);
        contextHolder.set(dbType);
    }

    // 獲取資料來源名
    public static String getDB() {
        return (contextHolder.get());
    }

    // 清除資料來源名
    public static void clearDB() {
        contextHolder.remove();
    }
}

第四步、通過編寫切面,對所有我們自定義切庫註解的方法進行攔截,動態的選擇資料來源

這裡是為下一步提供鋪墊,動態調整DataSourceContextHolder裡儲存的值,使用threadLocal來管理是為了避免多執行緒之間互相影響。

自定義註解,核心的處理就是寫處理這個註解的邏輯,然後通過指定的攔截方案根據當前的資料做一些動態的處理。比如Spring提供的@Controller、@Service等註解,都是需要我們在配置檔案裡配置好需要掃描的路徑,然後專案啟動時,spring根據配置去指定路徑讀取這些配置,然後這些類才可以被spring進行管理。

這裡不要忽略了預設資料來源要選擇主庫,如果切庫出現什麼問題,比如配置錯誤等,可以保證訪問主庫來得到正確的結果;另外,請求完了不要忘記呼叫提供的clearDB的操作,防止threadLocal誤用帶來的記憶體洩露。

@Aspect
@Component
@Slf4j
public class DynamicDataSourceAspect {

    @Before("@annotation(RoutingDataSource)")
    //注意:這個引數裡面要寫RoutingDataSource和註解裡面名稱保持一致,不然會報錯
    public void beforeSwitchDS(JoinPoint point,RoutingDataSource RoutingDataSource){

        //獲得當前訪問的class
        Class<?> className = point.getTarget().getClass();

        //獲得訪問的方法名
        String methodName = point.getSignature().getName();
        //得到方法的引數的型別
        Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
        String dataSource = DataSourceContextHolder.DEFAULT_DATASOURCE;
        try {
            // 得到訪問的方法物件
            Method method = className.getMethod(methodName, argClass);

            // 判斷是否存在@DS註解
            if (method.isAnnotationPresent(RoutingDataSource.class)) {
                RoutingDataSource annotation = method.getAnnotation(RoutingDataSource.class);
                // 取出註解中的資料來源名
                dataSource = annotation.value();
            }
        } catch (Exception e) {
            log.error("routing datasource exception, " + methodName, e);
        }
        // 切換資料來源
        DataSourceContextHolder.setDB(dataSource);
    }

    @After("@annotation(RoutingDataSource)")
    public void afterSwitchDS(JoinPoint point,RoutingDataSource RoutingDataSource){
        DataSourceContextHolder.clearDB();
    }
}

第五步、動態的取出我們在切面裡設定的資料來源的字串即可

這裡需要把原理介紹一下,在連線資料庫時其實是先選擇一個配置好的spring管理的datasource的id,就是我們之前在 DatasourceConfig 類裡定義的Datasource實體類的id:masterDB 和 slaveDB。然後根據id去spring的上下文選擇配置,進行資料庫連線。有興趣的可以看一下原始碼。

@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        log.debug("資料來源為{}", DataSourceContextHolder.getDB());
        return DataSourceContextHolder.getDB();
    }
}

第六步、取消自動配置資料來源,使用我們這裡定義的資料來源配置

在SpringBoot啟動類上通常直接使用@SpringBootApplication就可以了,這裡需要調整為:

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class
})

使用

如何使用呢,我們簡單演示一下:

@Service
public class DataSourceRoutingService {

    @Resource
    private SysUserMapper sysUserMapper;

    @RoutingDataSource(DataSources.MASTER_DB) // 這個註解這時是可以省略,因為預設就是訪問主庫
    public SysUser test1(int id) {
        return sysUserMapper.selectByPrimaryKey(id);
    }

    @RoutingDataSource(DataSources.SLAVE_DB)
    public SysUser test2(int id) {
        return sysUserMapper.selectByPrimaryKey(id);
    }
}

如此,資料庫切庫就OK了。如果你的系統已經有主庫、從庫之分了,那麼趕緊在你的系統裡利用起來吧。

擴充套件

這裡呢,還可以支援多個擴充套件。比如現在一個主庫後面有多個從庫,在切面拿到需要切換從庫時,還可以選擇隨機選擇一個,或者根據類名、方法名或業務配置等選擇某一個從庫,這樣不但可以分擔每個從庫的壓力,也可以有針對性的讓指定的讀請求打到指定的從庫上。如果有多個主庫,也可以有更多的選擇~

二、多個數據源的輕鬆支援

前面文章裡介紹了 自定義註解完成資料庫切庫 ,今天接著這個高併發的話題,繼續說一下專案裡多個數據源的支援。

如何理解支援多個數據源呢?簡單的說,就是一個專案裡,同時可以訪問多個不同的資料庫。

實現的原理先交待一下:單個數據源在配置時會繫結一套mybatis配置,多個數據源時,不同的資料來源繫結不同的mybatis配置就可以了,簡單的思路就是讓不同的資料來源掃描不同的包,讓不同的包下的mapper對應連線不同的資料來源去處理邏輯。

場景假設:專案底層有正常業務庫和日誌庫,希望解決的是將專案中的一些日誌單獨記錄到一個庫裡,比如使用者操作記錄、產品更新記錄等。

說一下為什麼會有這個需求:使用者操作記錄和產品更新記錄可能很多,而實際中使用的又很少,就只是在某些頁面單獨展示一下操作或更新記錄,絕大部分時間都在不停的做著插入操作,這時就可以把這種記錄放到業務核心庫外面。

自己還遇到一個場景,就是底層產品、訂單什麼的是存在不同的庫裡的,但是程式碼重構還沒做到產品相關的一個專案、訂單相關的一個專案這一步,這時候也可以考慮在一個專案裡同時支援多個數據源,訂單相關的類操作訂單庫,產品相關的類操作產品庫,前期做好配置就可以了,儘量別去跨庫join表,基本什麼都不影響。

不多說了,程式碼走起來 : (依舊使用springboot進行實現)

第一步、定義多個數據源的mybatis配置

application.properties 

mybatis.mapper-locations=mappers/*.xml
mybatisLog.mapper-locations=mappersLog/*.xml

## datasource master #
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test1?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=466420182

## datasource log #
spring.datasourceLog.type=com.alibaba.druid.pool.DruidDataSource
spring.datasourceLog.driver-class-name=com.mysql.jdbc.Driver
spring.datasourceLog.url=jdbc:mysql://localhost:3306/log?characterEncoding=UTF-8
spring.datasourceLog.username=root
spring.datasourceLog.password=466420182

第二步、定義多個數據源

@Configuration
public class DatasourceConfig {

    @Bean(destroyMethod =  "close", name = DataSources.MASTER_DB)
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }

    @Bean(destroyMethod =  "close", name = DataSources.LOG_DB)
    @ConfigurationProperties(prefix = "spring.datasourceLog")
    public DataSource dataSourceLog() {
        return DataSourceBuilder.create().type(DruidDataSource.class).build();
    }
}

第三步、分別配置多個數據源

@Configuration
@MapperScan(basePackages = {"com.mmall.practice.dao"})
public class MybatisConfig {

    @Autowired
    @Qualifier(DataSources.MASTER_DB)
    private DataSource masterDB;

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "mybatis")
    public SqlSessionFactoryBean sqlSessionFactoryBean() {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(masterDB);
        return sqlSessionFactoryBean;
    }
}
@Configuration
@MapperScan(basePackages = {"com.mmall.practice.daoLog"}, sqlSessionFactoryRef = "logSqlSessionFactory")
public class MybatisLogConfig {

    @Autowired
    @Qualifier("logDB")
    private DataSource logDB;

    @Bean(name = "logSqlSessionFactory")
    @ConfigurationProperties(prefix = "mybatisLog")
    public SqlSessionFactoryBean sqlSessionFactoryBean() {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(logDB);
        return sqlSessionFactoryBean;
    }
}

這裡需要注意兩個資料來源配置的差別,也是支援多資料來源的關鍵

1)Configuration 掃描不同的字首,取不同包下的sql對應的xml檔案

2)SqlSessionFactoryBean 例項化時,預設的額外添加了 @Primary註解

3)MapperScan 掃描的不同的包,如果掃描相同的包也能做,但是還需要做額外的配置,可以自己嘗試

4)不同的資料來源使用不同的SqlSessionFactoryBean例項

至此,不同包下面的 Mapper.java 檔案就可以連線不同的資料來源了。這裡就不說如何去使用了,和之前正常一樣去使用 Mapper.java 就可以了,只是操作的是不同的資料庫。

多資料來源支援是不是特別簡單,趕緊在你的專案裡用起來吧~

三 、mybatis 的 分表外掛shardbatis 實現 分表

  Mybatis中實現分表,有個很簡單的外掛,叫shardbatis,使用maven構建的工程,可以在pom.xml中新增依賴性即可:

<!-- 分庫分表外掛 -->
<dependency>
    <groupId>org.shardbatis</groupId>
    <artifactId>shardbatis</artifactId>
    <version>2.0.0B</version>
</dependency>
<!-- sql解析外掛 -->
<dependency>
    <groupId>net.sf.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>0.8.0</version>
</dependency>

 使用方式:shardbatis-2.0.0B.jar中有一個ShardStrategy介面,原始碼如下:

public abstract interface ShardStrategy
{
  public abstract String getTargetTableName(String paramString1, Object paramObject, String paramString2);
}

  使用者可以自定義實現該介面的getTargetTableName方法,例如按年月分表實現:

/**
 * 分表策略,自動按當前年月分表
 * @author yehx
 *
 */
public class ShardStrategyByYearMonthImpl implements ShardStrategy {
    
    private static Log log = LogFactory.getLog(ShardStrategyByYearMonthImpl.class);
    
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
    /**
     * 得到實際表名
     * @param baseTableName 邏輯表名,一般是沒有字首或者是字尾的表名
     * @param params        mybatis執行某個statement時使用的引數
     * @param mapperId      mybatis配置的statement id
     * @return
     */

    public String getTargetTableName(String baseTableName,Object params, String mapperId) {
        return baseTableName +"_" + sdf.format(new Date());
    }
}

  按年月分表的實現寫好了之後,那這時候就需要配置,哪個mapperId需要按照該策略進行操作,即需要配置shard_config.xml:

<shardingConfig>
    <!-- ignoreList可選配置 ignoreList配置的mapperId會被分表參加忽略解析,不會對sql進行修改,value可配置多個 -->
    <ignoreList>
        <value></value>
    </ignoreList>
    <!-- parseList可選配置 如果配置了parseList,只有在parseList範圍內並且不再ignoreList內的sql才會被解析和修改,value可配置多個 -->
    <parseList>
        <value>com.**.mapper.UserMapper.addUser</value>
    </parseList>
    <!-- 配置分表策略 -->
    <strategy tableName="demo_user"
        strategyClass="com.**.splittable.ShardStrategyByDayImpl" />
</shardingConfig> 

  那麼,如何載入該檔案呢,很簡單,只需要在mybatis-config.xml檔案中增加外掛配置即可,配置如下:

<configuration>
    <!-- 外掛配置 -->
    <plugins>
        <!-- 分表外掛配置 -->
        <plugin interceptor="com.google.code.shardbatis.plugin.ShardPlugin">  
                <property name="shardingConfig" value="shard_config.xml"/>  
        </plugin> 
    </plugins>
</configuration>

  而mybatis-config.xml檔案是在哪載入的呢?是在配置sqlSessionFactory的地方指定的,如用spring的話,則在spring-mybatis.xml中配置:

<!-- spring和MyBatis完美整合,不需要mybatis的配置對映檔案 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <!-- mybatis-config配置檔案 -->
    <property name="configLocation" value="classpath:mybatis-config.xml" />
    <!-- 自動掃描mapping.xml檔案 -->
    <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
</bean>

好了,配置ok,接下來就可以完美地實現分表的需求了!!!