Java充电社
专辑
博文
联系我
本人继续续收门徒,亲手指导
Spring事务专题
-> Spring实现数据库读写分离
1、JdbcTemplate实现增删改查
2、spring中编程式事务怎么用的?
3、spring声明事务怎么用的?
4、详解Spring事务中7种传播行为
5、Spring如何管理多数据源事务?
6、spring事务源码解析
7、@Transaction事务源码解析
8、实战篇:手把手带你实现事务消息!
9、spring事务拦截器顺序如何控制?
10、导致 Spring 事务失效常见的几种情况
11、Spring实现数据库读写分离
12、Spring集成MyBatis
上一篇:导致 Spring 事务失效常见的几种情况
下一篇:Spring集成MyBatis
<div style="display:none"></div> ## 1、背景 大多数系统都是读多写少,为了降低数据库的压力,可以对主库创建多个从库,从库自动从主库同步数据,程序中将写的操作发送到主库,将读的操作发送到从库去执行。 今天的主要目标:**通过spring实现读写分离**。 读写分离需实现下面2个功能: **1、由调用者通过参数来控制走主库还是从库** **2、未指定走哪个库的,默认走主库** ## 2、思考3个问题 **1、控制具体是读从库还是主库,如何实现?** 可以给方法添加一个参数,控制走从库还是主库。 **2、数据源如何路由?** spring-jdbc 包中提供了一个抽象类:AbstractRoutingDataSource,实现了javax.sql.DataSource接口,我们用这个类来作为数据源类,重点是这个类可以用来做数据源的路由,可以在其内部配置多个真实的数据源,最终用哪个数据源,由开发者来决定。 AbstractRoutingDataSource中有个map,用来存储多个目标数据源 ```java private Map<Object, DataSource> resolvedDataSources; ``` 比如主从库可以这么存储 ```java resolvedDataSources.put("master",主库数据源); resolvedDataSources.put("salave",从库数据源); ``` AbstractRoutingDataSource中还有抽象方法`determineCurrentLookupKey`,将这个方法的返回值作为key到上面的resolvedDataSources中查找对应的数据源,作为当前操作db的数据源 ```java protected abstract Object determineCurrentLookupKey(); ``` **3、读写分离在哪控制?** 读写分离属于一个通用的功能,可以通过spring的aop来实现,添加一个拦截器,拦截目标方法的之前,在目标方法执行之前,获取一下当前需要走哪个库,将这个标志存储在ThreadLocal中,将这个标志作为AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,截器中在目标方法执行完毕之后,将这个标志还原。 ## 3、代码实现 ### 3.1、工程结构图 ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/134/591f73cc-50f4-4d05-991c-3e66ff4809ff.png) ### 3.2、DsType 表示数据源类型,有2个值,用来区分是主库还是从库。 ```java package com.javacode2018.readwritesplit.base; public enum DsType { MASTER, SLAVE; } ``` ### 3.3、DsTypeHolder 内部有个ThreadLocal,用来记录当前走主库还是从库,将这个标志放在dsTypeThreadLocal中 ```java package com.javacode2018.readwritesplit.base; public class DsTypeHolder { private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>(); public static void setDsType(DsType dsType) { dsTypeThreadLocal.set(dsType); } public static void master() { setDsType(DsType.MASTER); } public static void slave() { setDsType(DsType.SLAVE); } public static DsType getDsType() { return dsTypeThreadLocal.get(); } } ``` ### 3.4、IService接口 这个接口起到标志的作用,当某个类需要启用读写分离的时候,需要实现这个接口,实现这个接口的类都会被读写分离拦截器拦截。 ```java package com.javacode2018.readwritesplit.base; //需要实现读写分离的service需要实现该接口 public interface IService { } ``` ### 3.5、ReadWriteDataSource 读写分离数据源,继承ReadWriteDataSource,注意其内部的determineCurrentLookupKey方法,从上面的ThreadLocal中获取当前需要走主库还是从库的标志。 ```java package com.javacode2018.readwritesplit.base; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.lang.Nullable; public class ReadWriteDataSource extends AbstractRoutingDataSource { @Nullable @Override protected Object determineCurrentLookupKey() { return DsTypeHolder.getDsType(); } } ``` ### 3.6、ReadWriteInterceptor 读写分离拦截器,需放在事务拦截器前面执行,通过@1代码我们将此拦截器的顺序设置为Integer.MAX_VALUE - 2,稍后我们将事务拦截器的顺序设置为Integer.MAX_VALUE - 1,事务拦截器的执行顺序是从小到达的,所以,ReadWriteInterceptor会在事务拦截器org.springframework.transaction.interceptor.TransactionInterceptor之前执行。 下面方法中会获取当前目标方法的最后一个参数,最后一个参数可以是DsType类型的,开发者可以通过这个参数来控制具体走主库还是从库。 ```java package com.javacode2018.readwritesplit.base; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.Objects; @Aspect @Order(Integer.MAX_VALUE - 2) //@1 @Component public class ReadWriteInterceptor { @Pointcut("target(IService)") public void pointcut() { } //获取当前目标方法的最后一个参数 private Object getLastArgs(final ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); if (Objects.nonNull(args) && args.length > 0) { return args[args.length - 1]; } else { return null; } } @Around("pointcut()") public Object around(final ProceedingJoinPoint pjp) throws Throwable { //获取当前的dsType DsType oldDsType = DsTypeHolder.getDsType(); try { //获取最后一个参数 Object lastArgs = getLastArgs(pjp); //lastArgs为SLAVE,走从库,其他的走主库 if (DsType.SLAVE.equals(lastArgs)) { DsTypeHolder.slave(); } else { DsTypeHolder.master(); } return pjp.proceed(); } finally { //退出的时候,还原dsType DsTypeHolder.setDsType(oldDsType); } } } ``` ### 3.7、ReadWriteConfiguration spring配置类,作用 1、@3:用来将com.javacode2018.readwritesplit.base包中的一些类注册到spring容器中,比如上面的拦截器ReadWriteInterceptor 2、@1:开启spring aop的功能 3、@2:开启spring自动管理事务的功能,@EnableTransactionManagement的order用来指定事务拦截器org.springframework.transaction.interceptor.TransactionInterceptor顺序,在这里我们将order设置为Integer.MAX_VALUE - 1,而上面ReadWriteInterceptor的order是Integer.MAX_VALUE - 2,所以ReadWriteInterceptor会在事务拦截器之前执行。 ```java package com.javacode2018.readwritesplit.base; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableAspectJAutoProxy //@1 @EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) //@2 @ComponentScan(basePackageClasses = IService.class) //@3 public class ReadWriteConfiguration { } ``` ### 3.8、@EnableReadWrite 这个注解用来开启读写分离的功能,@1通过@Import将ReadWriteConfiguration导入到spring容器了,这样就会自动启用读写分离的功能。业务中需要使用读写分离,只需要在spring配置类中加上@EnableReadWrite注解就可以了。 ```java package com.javacode2018.readwritesplit.base; import org.springframework.context.annotation.Import; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(ReadWriteConfiguration.class) //@1 public @interface EnableReadWrite { } ``` ## 4、案例 读写分离的关键代码写完了,下面我们来上案例验证一下效果。 ### 4.1、执行sql脚本 下面准备2个数据库:javacode2018_master(主库)、javacode2018_slave(从库) 2个库中都创建一个t_user表,分别插入了一条数据,稍后用这个数据来验证走的是主库还是从库。 ```java DROP DATABASE IF EXISTS javacode2018_master; CREATE DATABASE IF NOT EXISTS javacode2018_master; USE javacode2018_master; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '姓名' ); INSERT INTO t_user (name) VALUE ('master库'); DROP DATABASE IF EXISTS javacode2018_slave; CREATE DATABASE IF NOT EXISTS javacode2018_slave; USE javacode2018_slave; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '姓名' ); INSERT INTO t_user (name) VALUE ('slave库'); ``` ### 4.2、spring配置类 @1:启用读写分离 masterDs()方法:定义主库数据源 slaveDs()方法:定义从库数据源 dataSource():定义读写分离路由数据源 后面还有2个方法用来定义JdbcTemplate和事务管理器,方法中都通过@Qualifier("dataSource")限定了注入的bean名称为dataSource:即注入了上面dataSource()返回的读写分离路由数据源。 ```java package com.javacode2018.readwritesplit.demo1; import com.javacode2018.readwritesplit.base.DsType; import com.javacode2018.readwritesplit.base.EnableReadWrite; import com.javacode2018.readwritesplit.base.ReadWriteDataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @EnableReadWrite //@1 @Configuration @ComponentScan public class MainConfig { //主库数据源 @Bean public DataSource masterDs() { org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8"); dataSource.setUsername("root"); dataSource.setPassword("root123"); dataSource.setInitialSize(5); return dataSource; } //从库数据源 @Bean public DataSource slaveDs() { org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8"); dataSource.setUsername("root"); dataSource.setPassword("root123"); dataSource.setInitialSize(5); return dataSource; } //读写分离路由数据源 @Bean public ReadWriteDataSource dataSource() { ReadWriteDataSource dataSource = new ReadWriteDataSource(); //设置主库为默认的库,当路由的时候没有在datasource那个map中找到对应的数据源的时候,会使用这个默认的数据源 dataSource.setDefaultTargetDataSource(this.masterDs()); //设置多个目标库 Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DsType.MASTER, this.masterDs()); targetDataSources.put(DsType.SLAVE, this.slaveDs()); dataSource.setTargetDataSources(targetDataSources); return dataSource; } //JdbcTemplate,dataSource为上面定义的注入读写分离的数据源 @Bean public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } //定义事务管理器,dataSource为上面定义的注入读写分离的数据源 @Bean public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } ``` ### 4.3、UserService 这个类就相当于我们平时写的service,我是为了方法,直接在里面使用了JdbcTemplate来操作数据库,真实的项目操作db会放在dao里面,这个类实现了IService接口,调用每个方法的时候,都会被ReadWriteInterceptor拦截器处理,根据方法最后一个参数路由到对应的库。 ```java package com.javacode2018.readwritesplit.demo1; import com.javacode2018.readwritesplit.base.DsType; import com.javacode2018.readwritesplit.base.IService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Component public class UserService implements IService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private UserService userService; @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public String getUserNameById(long id, DsType dsType) { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); return (list != null && list.size() > 0) ? list.get(0) : null; } //这个insert方法会走主库,内部的所有操作都会走主库 @Transactional public void insert(long id, String name) { System.out.println(String.format("插入数据{id:%s, name:%s}", id, name)); this.jdbcTemplate.update("insert into t_user (id,name) values (?,?)", id, name); String userName = this.userService.getUserNameById(id, DsType.SLAVE); System.out.println("查询结果:" + userName); } @Transactional(propagation = Propagation.REQUIRED) public void test1(long id, DsType dsType) { { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); System.out.println(list); } this.userService.test2(id, DsType.MASTER); { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); System.out.println(list); } } //propagation为REQUIRES_NEW,开启一个新的事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public void test2(long id, DsType dsType) { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); System.out.println(list); } } ``` ### 4.4、测试用例 ```java package com.javacode2018.readwritesplit.demo1; import com.javacode2018.readwritesplit.base.DsType; import org.junit.Before; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class Demo1Test { UserService userService; @Before public void before() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig.class); context.refresh(); this.userService = context.getBean(UserService.class); } @Test public void test1() { System.out.println(this.userService.getUserNameById(1, DsType.MASTER)); System.out.println(this.userService.getUserNameById(1, DsType.SLAVE)); } @Test public void test2() { long id = System.currentTimeMillis(); System.out.println(id); this.userService.insert(id, "张三"); } @Test public void test3() throws SQLException { this.userService.test1(1, DsType.SLAVE); } } ``` test1方法执行2次查询,分别查询主库和从库,输出: ```java master库 slave库 ``` 是不是很爽,由开发者自己控制具体走主库还是从库。 test2执行结果如下,可以看出查询到了刚刚插入的数据,说明insert中所有操作都走的是主库。 ```java 1604905117467 插入数据{id:1604905117467, name:张三} 查询结果:张三 ``` 重点来了,运行一下test3,输出 ```java [slave库] [master库] [slave库] ``` test3方法中会调用this.userService.test1(1, DsType.SLAVE),要求走从库,再来看看userService.test1的方法,如下,比较特殊,有事务嵌套,有3次查询,第1次和第3次都在test1这个事务中运行,用的是同一个连接,都走的从库,而中间的一次查询调用的是`this.userService.test2(id, DsType.MASTER);`,注意这个test2方法上面标注了`@Transactional(propagation = Propagation.REQUIRES_NEW)`,会重新开启一个事务,又由于第2个参数的值是DsType.MASTER,所以其内部会走主库。 ```java @Transactional(propagation = Propagation.REQUIRED) public void test1(long id, DsType dsType) { { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); System.out.println(list); } //这里会调用this.userService.test2,会开启一个新事务,test2内部会走主库 this.userService.test2(id, DsType.MASTER); { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); System.out.println(list); } } //propagation为REQUIRES_NEW,开启一个新的事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public void test2(long id, DsType dsType) { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); System.out.println(list); } ``` ## 5、案例源码 ```java git地址: https://gitee.com/javacode2018/spring-series 本文案例对应源码: spring-series\lesson-004-readwritesplit ``` **本博客所有系列案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。** <a style="display:none" target="_blank" href="https://mp.weixin.qq.com/s/_S1DD2JADnXvpexxaBwLLg" style="color:red; font-size:20px; font-weight:bold">继续收门徒,亲手带,月薪 4W 以下的可以来找我</a> ## 最新资料 1. <a href="https://mp.weixin.qq.com/s?__biz=MzkzOTI3Nzc0Mg==&mid=2247484964&idx=2&sn=c81bce2f26015ee0f9632ddc6c67df03&scene=21#wechat_redirect" target="_blank">尚硅谷 Java 学科全套教程(总 207.77GB)</a> 2. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247484192&idx=1&sn=505f2faaa4cc911f553850667749bcbb&scene=21#wechat_redirect" target="_blank">2021 最新版 Java 微服务学习线路图 + 视频</a> 3. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247484573&idx=1&sn=7f3d83892186c16c57bc0b99f03f1ffd&scene=21#wechat_redirect" target="_blank">阿里技术大佬整理的《Spring 学习笔记.pdf》</a> 4. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247484544&idx=2&sn=c1dfe907cfaa5b9ae8e66fc247ccbe84&scene=21#wechat_redirect" target="_blank">阿里大佬的《MySQL 学习笔记高清.pdf》</a> 5. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247485167&idx=1&sn=48d75c8e93e748235a3547f34921dfb7&scene=21#wechat_redirect" target="_blank">2021 版 java 高并发常见面试题汇总.pdf</a> 6. <a href="https://mp.weixin.qq.com/s?__biz=MzkwOTAyMTY2NA==&mid=2247485664&idx=1&sn=435f9f515a8f881642820d7790ad20ce&scene=21#wechat_redirect" target="_blank">Idea 快捷键大全.pdf</a> ![](https://itsoku.oss-cn-hangzhou.aliyuncs.com/itsoku/blog/article/1/2883e86e-3eff-404a-8943-0066e5e2b454.png)
#custom-toc-container