我相信很多朋友都尝试写过读写分离插件,或者项目中用到过。首先读写分离的职责应该属于数据访问层而不是业务层,其次读写分离不应该侵入我们代码层中。因此在service—dao—orm—数据库驱动调用链中,要想插件不侵入我们的代码中,只能写在orm层和数据库驱动层,写在orm层就和具体orm框架耦合,写在数据库驱动层,就和具体数据库耦合。
在orm层实现读写分离还是在数据库驱动实现读写分离,主要看更换orm框架和数据库那个成本更高和实现难易程度。在此处不讨论那个更优,今天介绍的读写分离插件是基于mybatis框架实现的一写多读。基于springboot配置,因此在现有项目中集成非常方便,下载源码打成jar包引入到项目中,在springboot的配置文件中添加如下配置即可开启读写分离。
此插件一共做了三件事:数据源代理,数据源路由,分布式事务。
此插件对现有代码零侵入,要达到零侵入得益于代理模式。
首先数据源代理,读写分离在一个业务里面至少有两个数据源,读数据源,写数据源,但是在一个事务里面所有sql执行都是在同一个数据库连接下操作,因此需要实现DataSorce接口代理读写数据源: DataSourceProxy。DataSourceProxy类写操作时,返回写数据源的Connection,读操作时,返回读数据源的Connection。然而读写操作,要在真正执行数据库操作时才能确定,然而在真正在执行sql语句之前,就已经获取Connection操作,因此获取Connection操作时,应该返回一个代理的Connection,再实际执行sql语句时根据当前环境获取真实的Connetion。
因此DataSourceProxy返回的Connetion是一个代理类, 依赖一个DataSourceRout接口,在未执行sql语句之前都是由Connetion代理类完成操作。再执行sql语句时,由DataSourceRout接口返回具体Connetion执行sql语句,DataSourceRout接口只有一个getTargetDataSource方法,由具体实现类根据当前环境确定目标数据源,可能是读写数据源,也可能是分表后的具体目标数据源。
DataSourceRout接口目前有两个实现类,AbstractRWDataSourceRout实现读写分离,UserDataSourceRout实现根据不同的用户路由到不同的数据库组上。UserDataSourceRout这个类依赖一组AbstractRWDataSourceRout,实现读写分离。
具体类结构如下:
将DataSourceProxy注入到org.mybatis.spring.SqlSessionTemplate里面。Mybatis便实现读写分离。此时对现有代码完全透明。当然也可以注入到hibernate框架中,只不过需要自己实现DataSourceRout接口,DataSourceRout接口的实现类AbstractRWDataSourceRout是基于mybatis的。
通过org.mybatis.spring.SqlSessionTemplate这个类的源码查询,org.apache.ibatis.mapping.MappedStatement这个类里面的org.apache.ibatis.mapping.SqlCommandType这个域定义了mybatis执行sql语句类型,可以通过这个类确定当前操作是读操作还是写操作。
写一个mybatis的插件,在sql执行过程中通SqlCommandType这个类确定当前上下文是读操作还是写操作。把读写标记存入上下文中,在AbstractRWDataSourceRout这个类中拿取上下文中的读写标记返回对应的数据源,为了事务简单,保证当前上下文最多只有一个写连接和一个读连接,检查当前上下文是否有对应的数据库连接,如果没有相应的连接,获取连接,保存在当前上下文中,方便下次sql语句执行和事务执行。
数据源由原来单一数据源变成了一个读数据源和一个写数据源,事务也就变成了两个事务。Mybatis集成spring后,mybatis的事务交由spring管理,具体实现类是org.mybatis.spring.transaction.SpringManagedTransaction,为了和myabtis-spring无缝集成,采用代理模式,RWManagedTransaction继承SpringManagedTransaction,把事务分别委托给读写事务,整个线程只有一个读事务和一个写事务,读事务比较弱。因此分布式事务采用Best Efforts 1PC模式。
public void commit() throws SQLException {
Map<String, Connection> connectionMap = ConnectionHold.CONNECTION_CONTEXT.get();
Connection writeCon = connectionMap.remove(ConnectionHold.WRITE);
if(writeCon != null){
writeCon.commit();
}
Connection readCon = connectionMap.remove(ConnectionHold.READ);
if(readCon != null){
try {
readCon.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
第一个事务成功后,第二个事务有可能因为网络原因或者服务器宕机,不能执行成功,这个网络通讯的危险期虽然概率很小,但是也是个不可靠因素之一。由于整个会话中,只有一个写数据连接和一个读数据连接,读的事务性比较弱,只要写事务成功了,读事务失败影响不大,当然也可以不考虑读事务。因此先处理写事务,再处理读事务。
作为一个初级程序员,想要扩展现有的开源框架,其实不是那么困难,只要实现相应的接口,参考现有实现类实现接口,如果现有的实现类太复杂看不懂逻辑,其实也很好实现接口,就是把自己的实现委托给现有实现类,自己在委托前后做一些自己的业务逻辑。
此插件代码托管在
https://github.com/chenlei2/spring-boot-mybatis-rw
欢迎大家fork。
本文作者:陈雷(点融黑帮),promotion后端开发人员,热爱计算机,崇尚开源