52.Spring实现数据库读写分离

1、背景

大多数系统都是读多写少,为了降低数据库的压力,可以对主库创建多个从库,从库自动从主库同步数据,程序中将写的操作发送到主库,将读的操作发送到从库去执行。

今天的主要目标:通过spring实现读写分离

读写分离需实现下面2个功能:

1、由调用者通过参数来控制走主库还是从库

2、未指定走哪个库的,默认走主库

2、思考3个问题

1、控制具体是读从库还是主库,如何实现?

可以给方法添加一个参数,控制走从库还是主库。

2、数据源如何路由?

spring-jdbc 包中提供了一个抽象类:AbstractRoutingDataSource,实现了javax.sql.DataSource接口,我们用这个类来作为数据源类,重点是这个类可以用来做数据源的路由,可以在其内部配置多个真实的数据源,最终用哪个数据源,由开发者来决定。

AbstractRoutingDataSource中有个map,用来存储多个目标数据源

 
  1. private Map<Object, DataSource> resolvedDataSources;

比如主从库可以这么存储

 
  1. resolvedDataSources.put("master",主库数据源);
  2. resolvedDataSources.put("salave",从库数据源);

AbstractRoutingDataSource中还有抽象方法determineCurrentLookupKey,将这个方法的返回值作为key到上面的resolvedDataSources中查找对应的数据源,作为当前操作db的数据源

 
  1. protected abstract Object determineCurrentLookupKey();

3、读写分离在哪控制?

读写分离属于一个通用的功能,可以通过spring的aop来实现,添加一个拦截器,拦截目标方法的之前,在目标方法执行之前,获取一下当前需要走哪个库,将这个标志存储在ThreadLocal中,将这个标志作为AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,截器中在目标方法执行完毕之后,将这个标志还原。

3、代码实现

3.1、工程结构图

3.2、DsType

表示数据源类型,有2个值,用来区分是主库还是从库。

 
  1. package com.javacode2018.readwritesplit.base;
  2. public enum DsType {
  3. MASTER, SLAVE;
  4. }

3.3、DsTypeHolder

内部有个ThreadLocal,用来记录当前走主库还是从库,将这个标志放在dsTypeThreadLocal中

 
  1. package com.javacode2018.readwritesplit.base;
  2. public class DsTypeHolder {
  3. private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>();
  4. public static void setDsType(DsType dsType) {
  5. dsTypeThreadLocal.set(dsType);
  6. }
  7. public static void master() {
  8. setDsType(DsType.MASTER);
  9. }
  10. public static void slave() {
  11. setDsType(DsType.SLAVE);
  12. }
  13. public static DsType getDsType() {
  14. return dsTypeThreadLocal.get();
  15. }
  16. }

3.4、IService接口

这个接口起到标志的作用,当某个类需要启用读写分离的时候,需要实现这个接口,实现这个接口的类都会被读写分离拦截器拦截。

 
  1. package com.javacode2018.readwritesplit.base;
  2. //需要实现读写分离的service需要实现该接口
  3. public interface IService {
  4. }

3.5、ReadWriteDataSource

读写分离数据源,继承ReadWriteDataSource,注意其内部的determineCurrentLookupKey方法,从上面的ThreadLocal中获取当前需要走主库还是从库的标志。

 
  1. package com.javacode2018.readwritesplit.base;
  2. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  3. import org.springframework.lang.Nullable;
  4. public class ReadWriteDataSource extends AbstractRoutingDataSource {
  5. @Nullable
  6. @Override
  7. protected Object determineCurrentLookupKey() {
  8. return DsTypeHolder.getDsType();
  9. }
  10. }

3.6、ReadWriteInterceptor

读写分离拦截器,需放在事务拦截器前面执行,通过@1代码我们将此拦截器的顺序设置为Integer.MAX_VALUE - 2,稍后我们将事务拦截器的顺序设置为Integer.MAX_VALUE - 1,事务拦截器的执行顺序是从小到达的,所以,ReadWriteInterceptor会在事务拦截器org.springframework.transaction.interceptor.TransactionInterceptor之前执行。

下面方法中会获取当前目标方法的最后一个参数,最后一个参数可以是DsType类型的,开发者可以通过这个参数来控制具体走主库还是从库。

 
  1. package com.javacode2018.readwritesplit.base;
  2. import org.aspectj.lang.ProceedingJoinPoint;
  3. import org.aspectj.lang.annotation.Around;
  4. import org.aspectj.lang.annotation.Aspect;
  5. import org.aspectj.lang.annotation.Pointcut;
  6. import org.springframework.core.annotation.Order;
  7. import org.springframework.stereotype.Component;
  8. import java.util.Objects;
  9. @Aspect
  10. @Order(Integer.MAX_VALUE - 2) //@1
  11. @Component
  12. public class ReadWriteInterceptor {
  13. @Pointcut("target(IService)")
  14. public void pointcut() {
  15. }
  16. //获取当前目标方法的最后一个参数
  17. private Object getLastArgs(final ProceedingJoinPoint pjp) {
  18. Object[] args = pjp.getArgs();
  19. if (Objects.nonNull(args) && args.length > 0) {
  20. return args[args.length - 1];
  21. } else {
  22. return null;
  23. }
  24. }
  25. @Around("pointcut()")
  26. public Object around(final ProceedingJoinPoint pjp) throws Throwable {
  27. //获取当前的dsType
  28. DsType oldDsType = DsTypeHolder.getDsType();
  29. try {
  30. //获取最后一个参数
  31. Object lastArgs = getLastArgs(pjp);
  32. //lastArgs为SLAVE,走从库,其他的走主库
  33. if (DsType.SLAVE.equals(lastArgs)) {
  34. DsTypeHolder.slave();
  35. } else {
  36. DsTypeHolder.master();
  37. }
  38. return pjp.proceed();
  39. } finally {
  40. //退出的时候,还原dsType
  41. DsTypeHolder.setDsType(oldDsType);
  42. }
  43. }
  44. }

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会在事务拦截器之前执行。

 
  1. package com.javacode2018.readwritesplit.base;
  2. import org.springframework.context.annotation.ComponentScan;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.context.annotation.EnableAspectJAutoProxy;
  5. import org.springframework.transaction.annotation.EnableTransactionManagement;
  6. @Configuration
  7. @EnableAspectJAutoProxy //@1
  8. @EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) //@2
  9. @ComponentScan(basePackageClasses = IService.class) //@3
  10. public class ReadWriteConfiguration {
  11. }

3.8、@EnableReadWrite

这个注解用来开启读写分离的功能,@1通过@Import将ReadWriteConfiguration导入到spring容器了,这样就会自动启用读写分离的功能。业务中需要使用读写分离,只需要在spring配置类中加上@EnableReadWrite注解就可以了。

 
  1. package com.javacode2018.readwritesplit.base;
  2. import org.springframework.context.annotation.Import;
  3. import java.lang.annotation.*;
  4. @Target(ElementType.TYPE)
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Documented
  7. @Import(ReadWriteConfiguration.class) //@1
  8. public @interface EnableReadWrite {
  9. }

4、案例

读写分离的关键代码写完了,下面我们来上案例验证一下效果。

4.1、执行sql脚本

下面准备2个数据库:javacode2018_master(主库)、javacode2018_slave(从库)

2个库中都创建一个t_user表,分别插入了一条数据,稍后用这个数据来验证走的是主库还是从库。

 
  1. DROP DATABASE IF EXISTS javacode2018_master;
  2. CREATE DATABASE IF NOT EXISTS javacode2018_master;
  3. USE javacode2018_master;
  4. DROP TABLE IF EXISTS t_user;
  5. CREATE TABLE t_user (
  6. id INT PRIMARY KEY AUTO_INCREMENT,
  7. name VARCHAR(256) NOT NULL DEFAULT ''
  8. COMMENT '姓名'
  9. );
  10. INSERT INTO t_user (name) VALUE ('master库');
  11. DROP DATABASE IF EXISTS javacode2018_slave;
  12. CREATE DATABASE IF NOT EXISTS javacode2018_slave;
  13. USE javacode2018_slave;
  14. DROP TABLE IF EXISTS t_user;
  15. CREATE TABLE t_user (
  16. id INT PRIMARY KEY AUTO_INCREMENT,
  17. name VARCHAR(256) NOT NULL DEFAULT ''
  18. COMMENT '姓名'
  19. );
  20. INSERT INTO t_user (name) VALUE ('slave库');

4.2、spring配置类

@1:启用读写分离

masterDs()方法:定义主库数据源

slaveDs()方法:定义从库数据源

dataSource():定义读写分离路由数据源

后面还有2个方法用来定义JdbcTemplate和事务管理器,方法中都通过@Qualifier(“dataSource”)限定了注入的bean名称为dataSource:即注入了上面dataSource()返回的读写分离路由数据源。

 
  1. package com.javacode2018.readwritesplit.demo1;
  2. import com.javacode2018.readwritesplit.base.DsType;
  3. import com.javacode2018.readwritesplit.base.EnableReadWrite;
  4. import com.javacode2018.readwritesplit.base.ReadWriteDataSource;
  5. import org.springframework.beans.factory.annotation.Qualifier;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.ComponentScan;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.jdbc.core.JdbcTemplate;
  10. import org.springframework.jdbc.datasource.DataSourceTransactionManager;
  11. import org.springframework.transaction.PlatformTransactionManager;
  12. import javax.sql.DataSource;
  13. import java.util.HashMap;
  14. import java.util.Map;
  15. @EnableReadWrite //@1
  16. @Configuration
  17. @ComponentScan
  18. public class MainConfig {
  19. //主库数据源
  20. @Bean
  21. public DataSource masterDs() {
  22. org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
  23. dataSource.setDriverClassName("com.mysql.jdbc.Driver");
  24. dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8");
  25. dataSource.setUsername("root");
  26. dataSource.setPassword("root123");
  27. dataSource.setInitialSize(5);
  28. return dataSource;
  29. }
  30. //从库数据源
  31. @Bean
  32. public DataSource slaveDs() {
  33. org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
  34. dataSource.setDriverClassName("com.mysql.jdbc.Driver");
  35. dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8");
  36. dataSource.setUsername("root");
  37. dataSource.setPassword("root123");
  38. dataSource.setInitialSize(5);
  39. return dataSource;
  40. }
  41. //读写分离路由数据源
  42. @Bean
  43. public ReadWriteDataSource dataSource() {
  44. ReadWriteDataSource dataSource = new ReadWriteDataSource();
  45. //设置主库为默认的库,当路由的时候没有在datasource那个map中找到对应的数据源的时候,会使用这个默认的数据源
  46. dataSource.setDefaultTargetDataSource(this.masterDs());
  47. //设置多个目标库
  48. Map<Object, Object> targetDataSources = new HashMap<>();
  49. targetDataSources.put(DsType.MASTER, this.masterDs());
  50. targetDataSources.put(DsType.SLAVE, this.slaveDs());
  51. dataSource.setTargetDataSources(targetDataSources);
  52. return dataSource;
  53. }
  54. //JdbcTemplate,dataSource为上面定义的注入读写分离的数据源
  55. @Bean
  56. public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
  57. return new JdbcTemplate(dataSource);
  58. }
  59. //定义事务管理器,dataSource为上面定义的注入读写分离的数据源
  60. @Bean
  61. public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
  62. return new DataSourceTransactionManager(dataSource);
  63. }
  64. }

4.3、UserService

这个类就相当于我们平时写的service,我是为了方法,直接在里面使用了JdbcTemplate来操作数据库,真实的项目操作db会放在dao里面,这个类实现了IService接口,调用每个方法的时候,都会被ReadWriteInterceptor拦截器处理,根据方法最后一个参数路由到对应的库。

 
  1. package com.javacode2018.readwritesplit.demo1;
  2. import com.javacode2018.readwritesplit.base.DsType;
  3. import com.javacode2018.readwritesplit.base.IService;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.jdbc.core.JdbcTemplate;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.transaction.annotation.Propagation;
  8. import org.springframework.transaction.annotation.Transactional;
  9. import java.util.List;
  10. @Component
  11. public class UserService implements IService {
  12. @Autowired
  13. private JdbcTemplate jdbcTemplate;
  14. @Autowired
  15. private UserService userService;
  16. @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
  17. public String getUserNameById(long id, DsType dsType) {
  18. String sql = "select name from t_user where id=?";
  19. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  20. return (list != null && list.size() > 0) ? list.get(0) : null;
  21. }
  22. //这个insert方法会走主库,内部的所有操作都会走主库
  23. @Transactional
  24. public void insert(long id, String name) {
  25. System.out.println(String.format("插入数据{id:%s, name:%s}", id, name));
  26. this.jdbcTemplate.update("insert into t_user (id,name) values (?,?)", id, name);
  27. String userName = this.userService.getUserNameById(id, DsType.SLAVE);
  28. System.out.println("查询结果:" + userName);
  29. }
  30. @Transactional(propagation = Propagation.REQUIRED)
  31. public void test1(long id, DsType dsType) {
  32. {
  33. String sql = "select name from t_user where id=?";
  34. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  35. System.out.println(list);
  36. }
  37. this.userService.test2(id, DsType.MASTER);
  38. {
  39. String sql = "select name from t_user where id=?";
  40. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  41. System.out.println(list);
  42. }
  43. }
  44. //propagation为REQUIRES_NEW,开启一个新的事务
  45. @Transactional(propagation = Propagation.REQUIRES_NEW)
  46. public void test2(long id, DsType dsType) {
  47. String sql = "select name from t_user where id=?";
  48. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  49. System.out.println(list);
  50. }
  51. }

4.4、测试用例

 
  1. package com.javacode2018.readwritesplit.demo1;
  2. import com.javacode2018.readwritesplit.base.DsType;
  3. import org.junit.Before;
  4. import org.junit.Test;
  5. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  6. import javax.sql.DataSource;
  7. import java.sql.Connection;
  8. import java.sql.SQLException;
  9. public class Demo1Test {
  10. UserService userService;
  11. @Before
  12. public void before() {
  13. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  14. context.register(MainConfig.class);
  15. context.refresh();
  16. this.userService = context.getBean(UserService.class);
  17. }
  18. @Test
  19. public void test1() {
  20. System.out.println(this.userService.getUserNameById(1, DsType.MASTER));
  21. System.out.println(this.userService.getUserNameById(1, DsType.SLAVE));
  22. }
  23. @Test
  24. public void test2() {
  25. long id = System.currentTimeMillis();
  26. System.out.println(id);
  27. this.userService.insert(id, "张三");
  28. }
  29. @Test
  30. public void test3() throws SQLException {
  31. this.userService.test1(1, DsType.SLAVE);
  32. }
  33. }

test1方法执行2次查询,分别查询主库和从库,输出:

 
  1. master库
  2. slave库

是不是很爽,由开发者自己控制具体走主库还是从库。

test2执行结果如下,可以看出查询到了刚刚插入的数据,说明insert中所有操作都走的是主库。

 
  1. 1604905117467
  2. 插入数据{id:1604905117467, name:张三}
  3. 查询结果:张三

重点来了,运行一下test3,输出

 
  1. [slave库]
  2. [master库]
  3. [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,所以其内部会走主库。

 
  1. @Transactional(propagation = Propagation.REQUIRED)
  2. public void test1(long id, DsType dsType) {
  3. {
  4. String sql = "select name from t_user where id=?";
  5. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  6. System.out.println(list);
  7. }
  8. //这里会调用this.userService.test2,会开启一个新事务,test2内部会走主库
  9. this.userService.test2(id, DsType.MASTER);
  10. {
  11. String sql = "select name from t_user where id=?";
  12. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  13. System.out.println(list);
  14. }
  15. }
  16. //propagation为REQUIRES_NEW,开启一个新的事务
  17. @Transactional(propagation = Propagation.REQUIRES_NEW)
  18. public void test2(long id, DsType dsType) {
  19. String sql = "select name from t_user where id=?";
  20. List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
  21. System.out.println(list);
  22. }

5、案例源码

 
  1. git地址:
  2. https://gitee.com/javacode2018/spring-series
  3. 本文案例对应源码:
  4. spring-series\lesson-004-readwritesplit
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值