我相信很多朋友都尝试写过读写分离插件,或者项目中用到过。首先读写分离的职责应该属于数据访问层而不是业务层,其次读写分离不应该侵入我们代码层中。因此在 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 返回的 Connection 是一个代理类, 依赖一个 DataSourceRout 接口,在未执行sql语句之前都是由 Connection 代理类完成操作。再执行 sql 语句时,由 DataSourceRout 接口返回具体 Connection 执行 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 后端开发人员,热爱计算机,崇尚开源。
推荐阅读
本文作者陈雷,由点融黑帮公众号(微信id: DianrongMafia)授权高可用架构发表,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。
高可用架构
改变互联网的构建方式
长按二维码 关注「高可用架构」公众号