干货丨springboot环境实现读写分离

Posted 中兴开发者社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了干货丨springboot环境实现读写分离相关的知识,希望对你有一定的参考价值。

作者简介

作者黄永猛热衷各种软件架构,喜欢研究源码以及各种架构设计方案。该文主要是针对高并发的情况下数据层的高负载的一种解决方案。


读写分离使用的好处这里不做过多的阐述,相信看这篇文章的人都已经很清楚业务使用场景;下面我们直接进入主题:

     读写分离其实就是在底层替换数据源即可,针对一主一从的情况下数据源切换比较简单,那么在一主多从的情况下如何有效的切换数据源则是一件很头疼的事情,简单的算法可以用随机,稍微改进则可以用轮寻,再改进的是不是可以用minCollectsUsed?针对这些情况还是看业务场景决定吧;

下面是根据一主一从的场景下讲述:

  1. 首先要了解什么时候切换,如何切换

        在所有的读的时候进行切换,在update,delete,savfe的时候进行切换;如何判断这些呢?若是在集群的环境下我们可以通过路由控制服务的走向,在服务段配置下即可;但是这样移植性比较差;在不污染代码的同时我们可以采用AOP定义切入点来实现;实现方式如下:

 
   
   
 
  1. @Pointcut("execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.list*(..)) ||execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.count*(..))||execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.pager*(..))")  

  2.  public void slavePointCut(){}

  3.  

  4.  @Pointcut("execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.save*(..)) || execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.delete*(..))||execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.update*(..))||execution(* com.zte.wdcloud.mybatis.dao.impl.BaseDaoImpl.batchDelete*(..))")

  5.  public void masterPointCut(){}

  6.  

  7.  

  8. /*@Before("slavePointCut()")

  9. public void setSlaveDataSouce(){

  10. DataSourceContextHolder.read();

  11. }

  12. @Before("masterPointCut()")

  13. public void setMasterDataSource(){

  14. DataSourceContextHolder.write();

  15. }*/

  16. @Around("slavePointCut()")

  17. public Object processedSlave(ProceedingJoinPoint point) throws Throwable{

  18. try{

  19. System.out.println("--------------------------->set database source is slave");

  20. DataSourceContextHolder.read();

  21. Object result=point.proceed();

  22. return result;

  23. }finally {

  24. DataSourceContextHolder.removeDataSource();

  25. }

  26. }

  27. @Around("masterPointCut()")

  28. public Object processedMaster(ProceedingJoinPoint point) throws Throwable{

  29. try{

  30. System.out.println("---------------------------->set database source is master");

  31. DataSourceContextHolder.write();

  32. Object result=point.proceed();

  33. return result;

  34. }finally {

  35. DataSourceContextHolder.removeDataSource();

  36. }

  37. }

 aop的两种方式都可以实现(before和around),注释的是一种,可跟根据自己需求选择,在上述代码中有一个DatasourceContextHolder的类,里面定一个了一个线程副本,来存放读写的信息,代码如下:

 
   
   
 
  1. /**

  2.  * @author 黄永猛10221064

  3.  * 定义数据源之间的切换

  4.  */

  5. public class DataSourceContextHolder {

  6. private static final Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);

  7. private static final ThreadLocal<String> local=new ThreadLocal<String>();

  8. public static void read(){

  9. log.info("------------》切换到读库");

  10. local.set(DataSourceType.slaveDataSource.getDataSource());

  11. }

  12. public static void write(){

  13. log.info("------------》切换到写库");

  14. local.set(DataSourceType.masterDataSource.getDataSource());

  15. }

  16. public static String getCurrentDataSource(){

  17. log.info("------------》getCurrentDataSource");

  18. return local.get();

  19. }

  20. public static void removeDataSource(){

  21. local.remove();

  22. }

threadlocal的用法不多做解释,网上很多人的博客讲述的很详细;但是需要说明的一点,它不是本地线程这样来说的。。。


   2.数据源的定义以及数据源路由的控制

数据源的定义如下,springboot中的config信息就是将传统的springmvc中的xml代码化,看起来更方便;

 
   
   
 
  1. protected Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

  2. // @Scope("prototype")

  3. /*

  4.  * @Bean

  5.  * 

  6.  * @Primary public SqlSession SqlSession() throws Exception{ SqlSession

  7.  * sqlSession=new SqlSessionTemplate(sqlSessionFactory()); return

  8.  * sqlSession; }

  9.  */

  10. @Bean

  11. public SqlSessionFactory sqlSessionFactory() throws Exception {

  12. // return super.sqlSessionFactory(slaveDataSource());

  13. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

  14. sqlSessionFactoryBean.setDataSource(dataSouceProxy());

  15. // sqlSessionFactoryBean.setTypeAliasesPackage("com.*.*");

  16. sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("/config/mybatis-config.xml"));

  17. PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

  18. sqlSessionFactoryBean

  19. .setMapperLocations(resourcePatternResolver.getResources("classpath*:com/zte/wdcloud/mapper/*.xml"));

  20. return sqlSessionFactoryBean.getObject();

  21. }

  22. @Bean

  23. public AbstractRoutingDataSource dataSouceProxy() {

  24. MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();

  25. Map<Object, Object> targetDataSource = new HashMap<Object, Object>();

  26. targetDataSource.put(DataSourceType.slaveDataSource.getDataSource(), slaveDataSource());

  27. targetDataSource.put(DataSourceType.masterDataSource.getDataSource(), masterDataSource());

  28. proxy.setTargetDataSources(targetDataSource);

  29. proxy.setDefaultTargetDataSource(slaveDataSource());

  30. return proxy;

  31. }

  32. @Bean

  33. public PlatformTransactionManager PlatformTransactionManager() {

  34. return new DataSourceTransactionManager(dataSouceProxy());

  35. }

  36. @Value("${datasource.type}")

  37. private Class<? extends DataSource> dataSourceType;

  38. @Primary

  39. @Bean(name = "masterDataSource")

  40. @ConfigurationProperties(prefix = "datasource.master")

  41. public DataSource masterDataSource() {

  42. logger.info("-----初始化master数据库配置-----");

  43. return DataSourceBuilder.create().type(dataSourceType).build();

  44. }

  45. @Bean(name = "slaveDataSource")

  46. @ConfigurationProperties(prefix = "datasource.slave")

  47. public DataSource slaveDataSource() {

  48. logger.info("-----初始化slave数据库配置-----");

  49. return DataSourceBuilder.create().type(dataSourceType).build();

  50. }


MyAbstractRoutingDataSource类重写了AbstractRoutingDataSource中路由数据源;若是在多从的环境中,数据库调用负载均衡的算法则在此方法实现,下面看一下数据库的配置信息:

 
   
   
 
  1. datasource:

  2.   type: com.alibaba.druid.pool.DruidDataSource

  3.   master:

  4.     url: jdbc:mysql://10.67.18.17:3306/xxxxxx?useUnicode:true&characterEncoding:UTF-8

  5.     username: xxxxxxx

  6.     password: xxxxxxx

  7.     driver-class-name: com.mysql.jdbc.Driver

  8.     filters: stat

  9.     maxActive: 200

  10.     initialSize: 1

  11.     maxWait: 60000

  12.     minIdle: 1

  13.     timeBetweenEvictionRunsMillis: 60000

  14.     minEvictableIdleTimeMillis: 300000

  15.     validationQueryTimeout: 900000

  16.     validationQuery: SELECT SYSDATE() from dual

  17.     testWhileIdle: true

  18.     testOnBorrow: false

  19.     testOnReturn: false

  20.     poolPreparedStatements: true

  21.     maxOpenPreparedStatements: 20

  22.   slave:

  23.     url: jdbc:mysql://10.67.18.17:3306/yyyyyyyy?useUnicode:true&characterEncoding:UTF-8

  24.     username: xxxxx

  25.     password: xxxxxx

  26.     driver-class-name: com.mysql.jdbc.Driver

  27.     filters: stat

  28.     maxActive: 200

  29.     initialSize: 1

  30.     maxWait: 60000

  31.     minIdle: 1

  32.     timeBetweenEvictionRunsMillis: 60000

  33.     minEvictableIdleTimeMillis: 300000

  34.     validationQueryTimeout: 900000

  35.     validationQuery: SELECT SYSDATE() from dual

  36.     testWhileIdle: true

  37.     testOnBorrow: false

  38.     testOnReturn: false

  39.     poolPreparedStatements: true

  40.     maxOpenPreparedStatements: 20

哦,忘了说了,这里的数据源连接采用的是阿里的druid;

项目搭建没有采用以往的Spingjpa,而是用mybatis替换,个人认为是更灵活的~

以上是关于干货丨springboot环境实现读写分离的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot+ShardingSphereJDBC实现读写分离!

读写分离很难吗?springboot结合aop简单就实现了

SpringBoot+ShardingSphereJDBC实现读写分离!

SpringBoot+ShardingSphereJDBC实现读写分离!

SpringBoot+MyCat 实现读写分离分库分表主从同步

SpringBoot使用AOP,动态数据源实现数据库读写分离