一、前言

上一篇《MySQL 实现主从复制》 文章中介绍了 MySQL 主从复制的搭建,为了在项目上契合数据库的主从架构,本篇将介绍在应用层实现对数据库的读写分离。

二、原理

配置主从数据源,当接收请求时,执行具体方法之前(拦截),判断请求具体操作(读或写),最终确定从哪个数据源获取连接访问数据库。

在 JavaWeb 开发中,有 3 种方式可以对请求进行拦截:

  1. filter:拦截所有请求
  2. intercetor:拦截 handler/Action
  3. aop 切面:依赖切入点

不难看出,使用 AOP 切面进行拦截最合理和灵活,因此本文将介绍使用 AOP 实现读写分离功能。

三、编码

本文只张贴关键性代码,详细代码请下载文章末尾源码进行查看。

# 3.1 代码

1)DynamicDataSourceHolder 确保线程安全:

  1. /**
  2. *
  3. * 使用ThreadLocal技术来记录当前线程中的数据源的key
  4. *
  5. */
  6. public class DynamicDataSourceHolder {
  7. //写库对应的数据源key
  8. private static final String MASTER = "master";
  9. //读库对应的数据源key
  10. private static final String SLAVE = "slave";
  11. //使用ThreadLocal记录当前线程的数据源key
  12. private static final ThreadLocal<String> holder = new ThreadLocal<String>();
  13. /**
  14. * 设置数据源key
  15. * @param key
  16. */
  17. public static void putDataSourceKey(String key) {
  18. holder.set(key);
  19. }
  20. /**
  21. * 获取数据源key
  22. * @return
  23. */
  24. public static String getDataSourceKey() {
  25. return holder.get();
  26. }
  27. /**
  28. * 标记写库
  29. */
  30. public static void markMaster(){
  31. putDataSourceKey(MASTER);
  32. }
  33. /**
  34. * 标记读库
  35. */
  36. public static void markSlave(){
  37. putDataSourceKey(SLAVE);
  38. }
  39. }

2)定义 AOP 切面判断当前线程的读写操作

  1. /**
  2. * 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
  3. *
  4. */
  5. public class DataSourceAspect {
  6. /**
  7. * 在进入Service方法之前执行
  8. *
  9. * @param point 切面对象
  10. */
  11. public void before(JoinPoint point) {
  12. // 获取到当前执行的方法名
  13. String methodName = point.getSignature().getName();
  14. if (isSlave(methodName)) {
  15. // 标记为读库
  16. DynamicDataSourceHolder.markSlave();
  17. } else {
  18. // 标记为写库
  19. DynamicDataSourceHolder.markMaster();
  20. }
  21. }
  22. /**
  23. * 判断是否为读库
  24. *
  25. * @param methodName
  26. * @return
  27. */
  28. private Boolean isSlave(String methodName) {
  29. // 方法名以query、find、get开头的方法名走从库
  30. return StringUtils.startsWithAny(methodName, "query", "find", "get");
  31. }
  32. }

3)定义动态数据源,确定最终使用的数据源:

  1. /**
  2. * 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可
  3. *
  4. * 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
  5. *
  6. */
  7. public class DynamicDataSource extends AbstractRoutingDataSource{
  8. @Override
  9. protected Object determineCurrentLookupKey() {
  10. // 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
  11. String dataSourceKey = DynamicDataSourceHolder.getDataSourceKey();
  12. System.out.println("dataSourceKey ======> "+dataSourceKey);
  13. return dataSourceKey;
  14. }
  15. }

# 3.2 配置文件

1)jdbc.properties

  1. jdbc.driver=com.mysql.jdbc.Driver
  2. jdbc.master.url=jdbc:mysql://192.168.2.21/mysql_test?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=UTC
  3. jdbc.master.username=root
  4. jdbc.master.password=tiger
  5. jdbc.slave01.url=jdbc:mysql://192.168.2.22/mysql_test?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=UTC
  6. jdbc.slave01.username=root
  7. jdbc.slave01.password=tiger

2)applicationContext.xml

  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. xmlns:tx="http://www.springframework.org/schema/tx"
  6. xmlns:aop="http://www.springframework.org/schema/aop"
  7. xsi:schemaLocation="http://www.springframework.org/schema/beans
  8. http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  9. http://www.springframework.org/schema/context
  10. http://www.springframework.org/schema/context/spring-context-4.0.xsd
  11. http://www.springframework.org/schema/tx
  12. http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
  13. http://www.springframework.org/schema/aop
  14. http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
  15. <context:component-scan base-package="com.light.*">
  16. <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
  17. </context:component-scan>
  18. <context:property-placeholder location="classpath:*.properties"/>
  19. <!-- 数据源 -->
  20. <bean id="dataSource" class="com.light.dynamicdatasource.DynamicDataSource">
  21. <property name="targetDataSources">
  22. <map key-type="java.lang.String">
  23. <entry key="master" value-ref="masterDataSource"></entry>
  24. <entry key="slave" value-ref="slave01DataSource"></entry>
  25. </map>
  26. </property>
  27. <!-- 默认数据源 -->
  28. <property name="defaultTargetDataSource" ref="masterDataSource"/>
  29. </bean>
  30. <!-- 主库数据源 -->
  31. <bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
  32. <property name="url" value="${jdbc.master.url}"/>
  33. <property name="username" value="${jdbc.master.username}"/>
  34. <property name="password" value="${jdbc.master.password}"/>
  35. <property name="driverClassName" value="${jdbc.driver}"/>
  36. <property name="initialSize" value="5"/>
  37. <property name="minIdle" value="5"/>
  38. <property name="maxActive" value="50"/>
  39. </bean>
  40. <!-- 从库数据源 -->
  41. <bean id="slave01DataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
  42. <property name="url" value="${jdbc.slave01.url}"/>
  43. <property name="username" value="${jdbc.slave01.username}"/>
  44. <property name="password" value="${jdbc.slave01.password}"/>
  45. <property name="driverClassName" value="${jdbc.driver}"/>
  46. <property name="initialSize" value="5"/>
  47. <property name="minIdle" value="5"/>
  48. <property name="maxActive" value="50"/>
  49. </bean>
  50. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  51. <property name="dataSource" ref="dataSource"></property>
  52. <!-- 引入 mybatis 配置文件 -->
  53. <property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml"></property>
  54. <property name="typeAliasesPackage" value="com.light.domain"></property>
  55. <!-- sql配置文件 -->
  56. <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"></property>
  57. </bean>
  58. <!-- 扫描Mapper -->
  59. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  60. <property name="basePackage" value="com.light.mapper"></property>
  61. <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
  62. </bean>
  63. <!-- 事务管理器 -->
  64. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  65. <property name="dataSource" ref="dataSource"/>
  66. </bean>
  67. <!-- 通知 -->
  68. <tx:advice id="txAdvice" transaction-manager="transactionManager">
  69. <tx:attributes>
  70. <!-- 传播行为 -->
  71. <tx:method name="save*" propagation="REQUIRED"/>
  72. <tx:method name="insert*" propagation="REQUIRED"/>
  73. <tx:method name="delete*" propagation="REQUIRED"/>
  74. <tx:method name="update*" propagation="REQUIRED"/>
  75. <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
  76. <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
  77. <tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
  78. </tx:attributes>
  79. </tx:advice>
  80. <!-- 切面 -->
  81. <bean id="dataSourceAspect" class="com.light.dynamicdatasource.DataSourceAspect"></bean>
  82. <aop:config proxy-target-class="true">
  83. <aop:pointcut id="myPointcut" expression="execution(* com.light.service.*.*(..))" />
  84. <!-- 事务切面 -->
  85. <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"/>
  86. <!-- 自定义切面 -->
  87. <aop:aspect ref="dataSourceAspect" order="-9999">
  88. <aop:before method="before" pointcut-ref="myPointcut" />
  89. </aop:aspect>
  90. </aop:config>
  91. <tx:annotation-driven transaction-manager="transactionManager"/>
  92. </beans>

四、测试

笔者在项目的 web 层写了 UserController 类,里边包含 get 和 delete 两个方法。

正常情况,当访问 get 方法(读操作)时,使用从库数据源,那么控制台应该打印 slave 。

正常情况,当访问 delete 方法(写操作)时,使用主库数据源,那么控制台应该打印 master 。

以下是 2 次测试结果:

get 方法:

delete 方法:

五、源码

源码下载