读写分离
数据库读写分离是让主数据库处理事务性查询,而从数据库处理SELECT查询,也就是主数据库主要处理新增,修改,删除类的操作,当然也可以处理查询操作。通过增加物理机器,降低数据库的写压力,主从职责明确,很大程度避免了X锁和s锁的争用。主从分离,适用于可以接受一定程度的读延迟,主从同步是通过主库的binlog日志来同步数据的,会有一定的数据延迟。
方案简单对比分析
要实现读写分离就要考虑解决多数据源的问题。
a.可以采用Spring Transaction对多数据源支持的方案,在项目中配置多个数据源,多个sqlsesssion,在使用时通过注解指定数据源。
@Transactional("transactionManager1")
b.可以采用当当开源ShardingJDBC来实现多数据源的配置。
c.可以采用aop拦截,设定不同的业务采用不同的数据源。
对于方案a如果有的语句不需要在事务中执行,此时没有了注解哪么怎么确定采用哪种数据源呢?而且在事务嵌套中容易出现问题。方案b对事务支持不好,是弱事务的,也就是如果事务中间执行失败了,代码不会回滚,只会以尝试一定次数的方式来保证执行失败的任务重新执行,这对于幂等性、数据一致性都存在一定程度的问题。本文仅探讨aop的方式实现多数据源,从而实现读写分离。
spring aop实现
aop主要是为了在执行数据库操作之前拦截,然后设置数据源,这时考虑到事务的特点,所以对service层进行拦截。创建切面类DataSourceAspect,代码如下:
@Pointcut(".......")
public void dataSourceAspectj() {
}
@Around(value = "dataSourceAspectj()")
public Object aroundAdvice(final ProceedingJoinPoint point) throws Throwable {
try {
String dataSourceType = DataSourceTypeManager.getDataSourceType();
if (符合某种条件) {
DataSourceTypeManager.setDataSourceType(DataSourceTypeManager.DATA_SOURCE_MASTER);
}else{
DataSourceTypeManager.setDataSourceType(DataSourceTypeManager.DATA_SOURCE_SLAVER);
}
return point.proceed();
} finally {
DataSourceTypeManager.clearDataSourceType();
}
}
复制代码
DataSourceTypeManager类来保存数据源类型
public class DataSourceTypeManager {
public static String DATA_SOURCE_SLAVE = "slave";
public static String DATA_SOURCE_MASTER = "master";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
复制代码
看了上面的代码你可能存在疑问,设置了数据源类型,在哪里会用到呢?在执行sql语句之前,我们都知道需要先获取数据库的连接,生成statement等一系列步骤才能得到想要的结果,那么我们设置的数据源类型就是在获取数据库连接的时候用到的。获取数据的连接在AbstractRoutingDataSource类中,该类的源码部分如下(可以忽略不看):
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
。。。。。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
复制代码
而上面的代码determineCurrentLookupKey是个抽象方法,我们只需实现该抽象方法返回自己的数据源即可,因此定义DynamicDataSource类
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceTypeManager.getDataSourceType();
}
}
复制代码
DynamicDataSource继承了AbstractRoutingDataSource, 也是DataSource类,数据源配置的代码如下:
@Bean(name="dataSource")
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource
) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceTypeManager.DATA_SOURCE_SLAVE, slaveDataSource);
targetDataSources.put(DataSourceTypeManager.DATA_SOURCE_MASTER, masterDataSource);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(masterDataSource);
return dataSource;
}
复制代码
注意事项
当在service层调用dao层进行数据库处理时,若service没有启动事务机制,则执行的顺序为:切面——>determineCurrentLookupKey——>Dao方法。而当在service层启动事务时,由于在一个事务中执行失败后会回滚之前所执行的所有操作,因此spring会在service方法执行前调用determineCurrentLookupKey,那么需要在切面上设置如下注解,才能保证先执行切面。
@Order(1)