2018-01-08 学习随笔 SpirngBoot整合Mybatis进行主从数据库的动态切换,以及一些数据库层面和分布式事物的解决方案

时间:2021-03-27 02:27:35

先大概介绍一下主从数据库是什么?
其实就是两个或N个数据库,一个或几个主负责写(当然也可以读),另一个或几个从只负责读.从数据库要记录主数据库的具体url以及BigLOG(二进制日志文件)的参数.原理就是在
定时的从主数据库的BigLOG文件中获取相应的日志记录,并转换成相应的sql语句进行同步.

SpringBoot整合Mybatis怎么自动化的区分主从数据库进行读写,并且保证其线程安全性;
创建一个DataBaseContextHolder类,在类里定义一个成员变量ThreadLocal(一个可以绑定线程的Map,自动会以当前线程为Key,并将传入的值做浅克隆保存),
定义一个枚举,创建两个枚举常量(Master,Slave),并写上get,set方法,以及clean方法(getDataBaseType(),setDataBaseType());
public class DataBaseContextHolder {
public enum DataBaseType {
Master, Slave;
}

private static final ThreadLocal<DataBaseType> CONTEXTHOLDER = new ThreadLocal<>();

public static void setDataBaseType(DataBaseType dst) {
if (dst == null)
throw new NullPointerException();
CONTEXTHOLDER.set(dst);
}

public static DataBaseType getDataBaseType() {
return CONTEXTHOLDER.get() == null ? DataBaseType.Master : CONTEXTHOLDER.get();
}

public static void clearDataSource() {
CONTEXTHOLDER.remove();
}

之后创建一个Mybatis配置类,继承MyBatisAutoConfiguration(这是SpringBoot对Mybatis的默认配置类,所以我们可以继承它进行一个扩展),
注入主数据源和从数据源,并复写SqlSessionFactory()方法,在方法里调用父类的SqlSessionFactory方法,参数需要传入一个AbstractRoutingDataSource对象,
这个我们需要写一个自定义类,并且继承这个类,实现它的一个抽象方法determineCurrentLookupKey(),在方法中,直接调用DataBaseContextHolder.getDataBaseType(),
并返回它的返回值.
之后我们需要创建我们自定义类的对象,并通过ClassLoaderRepository对象的SoftHashMap方法获得一个SoftHashMap对象(一个线程安全,并且排序也就是存取有序的Map集合,也不知道它很LinklistHashMap有神马区别),
将我们的枚举,DataBaseContextHolder.DataBaseType.Master和DataBaseContextHolder.DataBaseType.Slave作为键,两个主从数据源作为值传入,我们自定义的AbstractRoutingDataSource子类对象调用setTargetDataSource(SoftHashMap)方法(记录指向数据源),
再指定一个默认数据源setDefaultDataSource(master),这个肯定指定主数据源,可读可写啊.
@Configuration
@AutoConfigureAfter({ DataBaseConfiguration.class })
public class MybatisConfiguration extends MybatisAutoConfiguration {
@Resource(name = "masterDataSource")
private DataSource masterDataSource;
@Resource(name = "slaveDataSource")
private DataSource slaveDataSource;

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory SqlSessionFactory() throws Exception {
return super.sqlSessionFactory(roundrobinDataSource());
}

public AbstractRoutingDataSource roundrobinDataSource() {
ReadWriterSplitRoutingDataSource rsd = new ReadWriterSplitRoutingDataSource();
SoftHashMap shm = new ClassLoaderRepository.SoftHashMap();
shm.put(DataBaseContextHolder.DataBaseType.Master, masterDataSource);
shm.put(DataBaseContextHolder.DataBaseType.Slave, slaveDataSource);
rsd.setDefaultTargetDataSource(masterDataSource);
rsd.setTargetDataSources(shm);
return rsd;
}
}

public class ReadWriterSplitRoutingDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DataBaseContextHolder.getDataBaseType();
}
}

当然做完这一切也仅仅是指定了配置而已,我们并没有在我们想切换数据源时就可以指定数据源的函数.这个我们可以通过注解加Aop的方式来实现;
创建一个注解,不用添加属性,只要指定其可以在方法和类上生效,生命周期的话需要指定为运行时.
之后创建一个Aop类,并拦截带有此注解的方法,在其运行之前更换当前ThreadLocal中保存的DataBaseType为Slave,并在方法执行完后清除ThreadLocal.

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnlyConnection {

}

@Aspect
@Component
public class ReadOnlyConnectionInterceptor implements Ordered {
private static final Logger log = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class);

@Around("@annotation(readOnlyConnection)")
public Object connetionProceed(ProceedingJoinPoint proceedingJoinPoint, ReadOnlyConnection readOnlyConnection)
throws Throwable {
try {
log.info("set database connection to readonly");
DataBaseContextHolder.setDataBaseType(DataBaseContextHolder.DataBaseType.Slave);
return proceedingJoinPoint.proceed();
} finally {
DataBaseContextHolder.clearDataSource();
log.info("restore database connection");
}
}

@Override
public int getOrder() {
return 0;
}

}
这个Order的话只是一个可以指定执行顺序(执行优先级)的接口,其实用@Order注解也可以指定,实现不实现都无所谓,不用管它.

普通视图,物化视图

普通视图的话只是定义,不是一个物理上的实表,并且不能做增删改操作,当我们查找普通视图上保存的数据的时候,是由数据库根据定义来生成sql在基表上进行查询的.
而物化视图则是一个实实在在的物理意义上的实表,和普通表一样可以创建索引,做增删改操作,并可以指定两种更新方式(就是讲基表上更新过的数据同步到物化视图上),OnDemand和OnCommit.
OnDemand的话也有两种一种是手动刷新,也就是需要我们代码调用存储函数,而自动刷新则可以在创建物化视图的时候直接指定时间,具体操作请百度.
OnCommit则是见其名知其义了,就是在基表发生commit事件的时候(任何事件完成后都会触发),就将基表数据同步更新到物化视图上.

个人观点:视图的创建除非是安全必要,否则没有必要且浪费性能,很多时候一个冗余字段就可以解决的事情就不必做跨表或者跨库(Oracle专属)的视图了;

分表,分库
水平拆分:按行拆分,id若为int自增可以直接利用取膜再查表.为UUID类字符型,则需要先计算出其HASH值再取膜查找是那个表;
垂直拆分:根据字段关联性拆分;

当每日增长新数据很大的时候,可以利用存储过程加定时任务,进行一个周期性的建表;

分布式事物要解决的就是两个不同数据源之间保存的数据的最终一致性,实现方式只能通过合理的代码设计(例如:加版本号字段实现乐观锁)