您的位置:首页 > 数据库

基于客户端(非proxy)实现数据库读写分离

2018-01-17 15:00 369 查看
之前写过一篇文章是基于atlas实现数据库读写分离(文章链接),还有一篇是主从同步(文章链接),atlas是一个proxy中间件,它的结构是在数据库与业务中间加了个中间件。

数据库读写分离有两种分类,其中一种就是上面所说的基于proxy,而另一种就是这篇文章要讲到的基于客户端的,什么是基于客户端?基于客户端意思就是去proxy中心话,把读写分离代码实现在业务中实现,这种方式缺点就是侵入性比较强,但性能好。基于客户端实现读写分离框架有淘宝的TDDL以及当当的sharding-jdbc,不过这篇讲的不是这些框架的运用,而是手写一个比较简单的实现。

下面写的代码都是基于之前ssm框架的demo,我直接拿以前的demo来进行实现数据库读写分离实现,主要代码如下:



主要代码也就标红的三个类,以及applicationContext.xml这个文件加了点配置

好了,撸起袖子就是干,把代码贴出来:

1.DBHolder实现:

package com.cwh.db;

public class DBHolder {

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

private static String DEFAUL_BD = "writerDataSource";

public static String getDbType(){
String db = holder.get();
if(db == null){
return DEFAUL_BD;
}
return db;
}

public static void setDbType(String db){
holder.set(db);
}
}

2.DynamicDataSource实现

package com.cwh.db;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource{

@Override
protected Object determineCurrentLookupKey() {
System.out.println("=====use db:"+DBHolder.getDbType());
return DBHolder.getDbType();
}

}

3.DynamicDataSourceAop实现:

package com.cwh.db;

import java.util.Map;

import org.aspectj.lang.JoinPoint;

public class DynamicDataSourceAop {

Map<String, String> methods;

String defaultDataSource;

public Map<String, String> getMethods() {
return methods;
}

public void setMethods(Map<String, String> methods) {
this.methods = methods;
}

public String getDefaultDataSource() {
return defaultDataSource;
}

public void setDefaultDataSource(String defaultDataSource) {
if(defaultDataSource == null || "".equals(defaultDataSource)){
throw new NullPointerException("defaultDataSource 不允许为空");
}
this.defaultDataSource = defaultDataSource;
}

public String getDbTypeKey(String method) {
method = method.toUpperCase();
for (String m : methods.keySet()) {
String mt = m.toUpperCase();
if (!m.contains("*") && mt.equals(method)
|| method.startsWith(mt.substring(0, mt.indexOf("*") - 1))
|| method.equals(mt.substring(0, mt.indexOf("*") - 1))) {
return methods.get(m);
}
}
return defaultDataSource;
}

public void dynamicDataSource(JoinPoint jp){
DBHolder.setDbType(getDbTypeKey(jp.getSignature().getName()));
}

}

4.修改applicationContext.xml配置:

<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd ">
<context:annotation-config/>
<context:component-scan base-package="com.cwh.*"/>
<!-- <tx:annotation-driven proxy-target-class="true"/> -->
<context:property-placeholder location="classpath:*.properties"/><!-- 加载配置文件 -->
<bean id="writerDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.43.42:3306/mybatis?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value=""/>
<!-- 连接被泄露时是否打印 -->
<property name="logAbandoned" value="true"/>
<property name="initialSize" value="2"/>
<property name="maxActive" value="2"/>
<!--minIdle: 最小空闲连接-->
<property name="minIdle" value="2"/>
<!--maxIdle: 最大空闲连接-->
<property name="maxIdle" value="2"/>
<!--maxWait: 超时等待时间以毫秒为单位 1000等于60秒-->
<property name="maxWait" value="1000"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
<bean id="readerDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value=""/>
<!-- 连接被泄露时是否打印 -->
<property name="logAbandoned" value="true"/>
<property name="initialSize" value="2"/>
<property name="maxActive" value="2"/>
<!--minIdle: 最小空闲连接-->
<property name="minIdle" value="2"/>
<!--maxIdle: 最大空闲连接-->
<property name="maxIdle" value="2"/>
<!--maxWait: 超时等待时间以毫秒为单位 1000等于60秒-->
<property name="maxWait" value="1000"/>
<property name="poolPreparedStatements" value="true"/>
</bean>

<!--  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource" />
</bean> -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--dataSource属性指定要用到的连接池-->
<property name="dataSource" ref="dynamicDataSource"/>
<!-- 所有配置的mapper文件 -->
<property name="mapperLocations" value="classpath*:com/cwh/mapper/*.xml" />
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.cwh.inter" />
</bean>

<bean id="dynamicDataSource" class="com.cwh.db.DynamicDataSource">
<property name="targetDataSources">
<map>
<entry value-ref="writerDataSource" key="writerDataSource"></entry>
<entry value-ref="readerDataSource" key="readerDataSource"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="writerDataSource"></property>
</bean>

<bean id="dynamicDataSourceAop" class="com.cwh.db.DynamicDataSourceAop">
<property name="methods">
<map>
<entry key="select*" value="readerDataSource"></entry>
<entry key="find*" value="readerDataSource"></entry>
<entry key="get*" value="readerDataSource"></entry>
</map>
</property>
<property name="defaultDataSource" value="writerDataSource"></property>
</bean>

<aop:config expose-proxy="true">
<aop:pointcut expression="execution(* com.cwh.service..*.*(..))" id="pointcut"/>
<aop:aspect ref="dynamicDataSourceAop">
<aop:before method="dynamicDataSource" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>

</beans>

说明:首先写了两个数据源DataSource,分别是writerDataSource写库,也就是master主库数据源,以及

readerDataSource读库,也就是slave从库数据源。以及我们上面我们实现的继承AbstractRoutingDataSource,这个很重要,这个是spring提供的,里面需要实现方法:determineCurrentLookupKey(),该类里面还有两个重要属性targetDataSources以及默认数据源defaultTargetDataSource,把我们读写数据源传进targetDataSources,以及默认数据源为writerDataSource主库。然后把dynamicDataSource传给sqlSessionFactory的dataSource。以及实现一个切面aop把我们需要拦截判断是采用走哪个库进行判断。切面于service包下的所有方法,find前缀、get前缀、以及select前缀等方法名的将走从库,其他全部走主库

4.测试

package com.cwh.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.cwh.model.User;
import com.cwh.service.UserService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:com/cwh/config/applicationContext.xml"})
public class DemoTest {

@Autowired
private UserService userService;

@Test
public void get(){
User user = userService.getUser("1");
System.out.println(user.getUserName());
}

@Test
public void insert(){
User user = new User();
user.setId(3);
user.setUserAge("22");
user.setUserName("menco");
userService.insertUser(user);
}
}

说明:当我们执行get测试也就是调用service的getUser方法,以get为前缀的方法,那么我们预判结果将走从库,执行结果如下:



果然没错,走的就是从库读库;

接着我们执行下insert测试,执行结果如下:





5.ok这样具体实现一个粗略的读写分离,但这种实现有一个比较明显的缺点就是当读操作方法里面调用了写操作,那么还是走了从库,当然我建议还是可以把颗粒度降到最低的,例如如果读操作里调用了写操作那么就强制走主库,还有上面我们提到的可以用TDDL或者sharding-jdbc框架来做,这些框架比较成熟。还有要提的一点是以上操作当然是要基于主从同步的啦,具体实现之前文章写过(文章链接)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息