1 背景

我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案,

其中一个是主库,负责写入数据,我们称之为:写库;

其它都是从库,负责读取数据,我们称之为:读库;

2 方案

解决读写分离的方案有两种:应用层解决和中间件解决。

2.1 应用层解决:

优点:

多数据源切换方便,由程序自动完成;

不需要引入中间件;

理论上支持任何数据库

缺点:

由程序员完成,运维参与不到;

不能做到动态增加数据源;

2.2 中间件解决:

优点:

源程序不需要做任何改动就可以实现读写分离;

动态添加数据源不需要重启程序;

缺点:

程序依赖于中间件,会导致切换数据库变得困难;

由中间件做了中转代理,性能有所下降;

相关中间件产品使用:

mysql-proxy

Amoeba for MySQL

3 使用Spring基于应用层实现

3.1

在进入Service之前,使用AOP来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库。

3.2 DynamicDataSource:

/**

 * @author kris

 * @create 2018-02-23 10:53

 * 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可

 * 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。

 */

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override

    protected Object determineCurrentLookupKey() {

        // 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key

        return DynamicDataSourceHolder.getDataSourceKey();

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

3.3 DynamicDataSourceHolder:

/**

 * @author kris

 * @create 2018-02-23 10:54

 * 使用ThreadLocal技术来记录当前线程中的数据源的key

 */

public class DynamicDataSourceHolder {

    //写库对应的数据源key

    private static final String MASTER = "master";

    //读库对应的数据源key

    private static final String SLAVE = "slave";

    //使用ThreadLocal记录当前线程的数据源key

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

    /**

     * 设置数据源key

     * @param key

     */

    public static void putDataSourceKey(String key) {

        holder.set(key);

    }

    /**

     * 获取数据源key

     * @return

     */

    public static String getDataSourceKey() {

        return holder.get();

    }

    /**

     * 标记写库

     */

    public static void markMaster(){

        putDataSourceKey(MASTER);

    }

    /**

     * 标记读库

     */

    public static void markSlave(){

        putDataSourceKey(SLAVE);

    }

}

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

3.4 DataSourceAspect:

/**

 * @author kris

 * @create 2018-02-23 10:57

 * 定义数据源的AOP切面,通过该Service的方法名判断是应该走主库还是从库

 */

public class DataSourceAspect {

    /**

     * 在进入Service方法之前执行

     *

     * @param point 切面对象

     */

    public void before(JoinPoint point) {

        // 获取到当前执行的方法名

        String methodName = point.getSignature().getName();

        if (isSlave(methodName)) {

            // 标记为主库

            DynamicDataSourceHolder.markSlave();

        } else {

            // 标记为次库

            DynamicDataSourceHolder.markMaster();

        }

    }

    /**

     * 判断是否为次库

     *

     * @param methodName

     * @return

     */

    private Boolean isSlave(String methodName) {

        // 方法名以query、find、get开头的方法名走从库

        return StringUtils.startsWithAny(methodName, "query","find","get");

    }

}

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

3.5 配置2个数据源:

3.5.1 jdbc.properties:

jdbc.master.driver=com.mysql.jdbc.Driver

jdbc.master.url=jdbc:mysql://localhost:3306/market?useUnicode=true&amp;characterEncoding=UTF-8

jdbc.master.username=root

jdbc.master.password=123456

jdbc.slave01.driver=com.mysql.jdbc.Driver

jdbc.slave01.url=jdbc:mysql://localhost:3307/market?useUnicode=true&amp;characterEncoding=UTF-8

jdbc.slave01.username=root

jdbc.slave01.password=123456

1

2

3

4

5

6

7

8

9

3.5.2 定义连接池:

<!-- 数据库连接池 -->

<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource"

      destroy-method="close">

    <property name="url" value="${jdbc.master.url}" />

    <property name="username" value="${jdbc.master.username}" />

    <property name="password" value="${jdbc.master.password}" />

    <property name="driverClassName" value="${jdbc.master.driver}" />

    <!-- 初始化连接大小 -->

    <property name="initialSize" value="5"/>

    <!-- 初始化连接池最大使用连接数量 -->

    <property name="maxActive" value="20"/>

    <!-- 初始化连接池最小空闲 -->

    <property name="minIdle" value="5"/>

    <!-- 获取连接最大等待时间,单位毫秒-->

    <property name="maxWait" value="60000"/>

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->

    <property name="timeBetweenEvictionRunsMillis" value="60000"/>

    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->

    <property name="minEvictableIdleTimeMillis" value="25200000"/>

    <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->

    <!--如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。分库分表较多的数据库,建议配置为false。-->

    <property name="poolPreparedStatements" value="false" />

    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />

</bean>

<bean id="slave01DataSource" class="com.alibaba.druid.pool.DruidDataSource"

      destroy-method="close">

    <property name="url" value="${jdbc.slave01.url}" />

    <property name="username" value="${jdbc.slave01.username}" />

    <property name="password" value="${jdbc.slave01.password}" />

    <property name="driverClassName" value="${jdbc.slave01.driver}" />

    <!-- 初始化连接大小 -->

    <property name="initialSize" value="5"/>

    <!-- 初始化连接池最大使用连接数量 -->

    <property name="maxActive" value="20"/>

    <!-- 初始化连接池最小空闲 -->

    <property name="minIdle" value="5"/>

    <!-- 获取连接最大等待时间,单位毫秒-->

    <property name="maxWait" value="60000"/>

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->

    <property name="timeBetweenEvictionRunsMillis" value="60000"/>

    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->

    <property name="minEvictableIdleTimeMillis" value="25200000"/>

    <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->

    <!--如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。分库分表较多的数据库,建议配置为false。-->

    <property name="poolPreparedStatements" value="false" />

    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />

</bean>

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

3.5.3 定义DataSource:

<!-- 定义数据源,使用自己实现的数据源 -->

<bean id="dataSource" class="com.market.web.Interceptor.DynamicDataSource">

    <!-- 设置多个数据源 -->

    <property name="targetDataSources">

        <map key-type="java.lang.String">

            <!-- 这个key需要和程序中的key一致 -->

            <entry key="master" value-ref="masterDataSource"/>

            <entry key="slave" value-ref="slave01DataSource"/>

        </map>

    </property>

    <!-- 设置默认的数据源,这里默认走写库 -->

    <property name="defaultTargetDataSource" ref="masterDataSource"/>

</bean>

1

2

3

4

5

6

7

8

9

10

11

12

13

3.6 配置事务管理以及动态切换数据源切面:

3.6.1 定义事务管理器:

<!-- 事务管理器 -->

<bean id="transactionManager"

      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <!-- 数据源 -->

    <property name="dataSource" ref="dataSource" />

</bean>

1

2

3

4

5

6

3.6.2 定义事务策略:

<!-- 通知 -->

<tx:advice id="txAdvice" transaction-manager="transactionManager">

    <tx:attributes>

        <!-- 传播行为 -->

        <tx:method name="save*" propagation="REQUIRED" />

        <tx:method name="insert*" propagation="REQUIRED" />

        <tx:method name="add*" propagation="REQUIRED" />

        <tx:method name="create*" propagation="REQUIRED" />

        <tx:method name="delete*" propagation="REQUIRED" />

        <tx:method name="update*" propagation="REQUIRED" />

        <tx:method name="find*" propagation="SUPPORTS" read-only="true" />

        <tx:method name="select*" propagation="SUPPORTS" read-only="true" />

        <tx:method name="get*" propagation="SUPPORTS" read-only="true" />

        <!--其他方法使用默认事务策略 -->

        <tx:method name="*" />

    </tx:attributes>

</tx:advice>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

3.6.3 定义切面:

<!-- 定义AOP切面处理器 -->

<bean class="com.market.web.Interceptor.DataSourceAspect" id="dataSourceAspect" />

<!-- 切面 -->

<aop:config>

    <!-- 定义切面,所有的service的所有方法 -->

    <aop:pointcut id="txPointcut" expression="execution(* com.market.service.*.*(..))" />

    <!-- 应用事务策略到Service切面 -->

    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>

    <!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 -->

    <aop:aspect ref="dataSourceAspect" order="-9999">

        <aop:before method="before" pointcut-ref="txPointcut" />

    </aop:aspect>

</aop:config>

版权声明:本文为博主「一条很老的腊肉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。