读写分离注解
Posted juncaoit
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读写分离注解相关的知识,希望对你有一定的参考价值。
以前写过读写分离,今天完善成文档。
一:概述
1.结构文档
2.思路
组装好各个数据源,然后通过注解选择使用读或者写的数据源,将其使用AbstractRoutingDataSource中的方法determineCurrentLookuoKey进行选择datasource的key。
然后,通过key,就找到了要使用的数据源。
在数据源的这里,最后配置上连接池。
3.说明
本配置直接使用一个公共的连接池配置,如果需要,自己进行配置
二:程序说明
1.连接池配置
package com.jun.webpro.common.config.dataSource.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; /** * 数据库连接池配置 */ @ConfigurationProperties(prefix = "spring.druid") @Component @Data public class DruidProperties { /** * 初始化大小 */ private int initialSize; /** * 最小 */ private int minIdle; /** * 最大 */ private int maxActive; /** * 获取连接等待超时的时间 */ private int maxWait; /** * 间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ private int timeBetweenEvictionRunsMillis; /** * 一个连接在池中最小生存的时间,单位是毫秒 */ private int minEvictableIdleTimeMillis; private String validationQuery; private boolean testWhileIdle; private boolean testOnBorrow; private boolean testOnReturn; /** * 打开PSCache */ private boolean poolPreparedStatements; /** * 并且指定每个连接上PSCache的大小 */ private int maxPoolPreparedStatementPerConnectionSize; /** * 监控统计拦截的filters,去掉后监控界面sql无法统计,‘wall‘用于防火墙 */ private String filters; /** * 物理连接初始化的时候执行的sql */ private List<String> connectionInitSqls; /** * connectProperties属性来打开mergeSql功能;慢SQL记录 */ private Map<String, String> connectionProperties; }
2.主从库的配置项接口
因为通过这个接口,进行下面的注入
package com.jun.webpro.common.config.dataSource.properties; public interface DataSourceProperties { String getDriverClassName(); String getUrl(); String getUsername(); String getPassword(); }
3.从库配置
package com.jun.webpro.common.config.dataSource.properties; import lombok.Data; import lombok.Getter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @Getter @ConfigurationProperties("mysql.datasource.slave") public class SlaveDataSourceProperties implements DataSourceProperties{ /** * url */ private String url; /** * driverClassName */ private String driverClassName; /** * username */ private String username; /** * password */ private String password; }
4.主库配置
package com.jun.webpro.common.config.dataSource.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @ConfigurationProperties("mysql.datasource.master") public class MasterDataSourceProperties implements DataSourceProperties{ /** * url */ private String url; /** * driverClassName */ private String driverClassName; /** * username */ private String username; /** * password */ private String password; }
5.数据池配置项
package com.jun.webpro.common.config.dataSource.config; import com.alibaba.druid.pool.DruidDataSource; import com.jun.webpro.common.config.dataSource.properties.DataSourceProperties; import com.jun.webpro.common.config.dataSource.properties.DruidProperties; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; import java.sql.SQLException; import java.util.Properties; @Configuration public class DataSourceConfig { // 导入数据库连接池配置 @Resource private DruidProperties druidProperties; /** * @param dataSourceProperties 其他地方传参 */ DruidDataSource initDruidDataSource(DataSourceProperties dataSourceProperties) throws SQLException { try (DruidDataSource dataSource = new DruidDataSource()) { dataSource.setInitialSize(druidProperties.getInitialSize()); dataSource.setMinIdle(druidProperties.getMinIdle()); dataSource.setMaxActive(druidProperties.getMaxActive()); dataSource.setMaxWait(druidProperties.getMaxWait()); dataSource.setTimeBetweenEvictionRunsMillis(druidProperties.getTimeBetweenEvictionRunsMillis()); dataSource.setMinEvictableIdleTimeMillis(druidProperties.getMinEvictableIdleTimeMillis()); dataSource.setValidationQuery(druidProperties.getValidationQuery()); dataSource.setTestWhileIdle(druidProperties.isTestWhileIdle()); dataSource.setTestOnBorrow(druidProperties.isTestOnBorrow()); dataSource.setTestOnReturn(druidProperties.isTestOnReturn()); dataSource.setPoolPreparedStatements(druidProperties.isPoolPreparedStatements()); dataSource.setMaxPoolPreparedStatementPerConnectionSize(druidProperties.getMaxPoolPreparedStatementPerConnectionSize()); dataSource.setConnectionInitSqls(druidProperties.getConnectionInitSqls()); dataSource.setFilters(druidProperties.getFilters()); Properties properties = new Properties(); for (String key : druidProperties.getConnectionProperties().keySet()) { properties.setProperty(key, druidProperties.getConnectionProperties().get(key)); } dataSource.setConnectProperties(properties); dataSource.setDriverClassName(dataSourceProperties.getDriverClassName()); dataSource.setUrl(dataSourceProperties.getUrl()); dataSource.setUsername(dataSourceProperties.getUsername()); dataSource.setPassword(dataSourceProperties.getPassword()); return dataSource; } } }
6.主数据库配置项
package com.jun.webpro.common.config.dataSource.config; import com.jun.webpro.common.config.dataSource.properties.MasterDataSourceProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.annotation.Resource; import javax.sql.DataSource; import java.sql.SQLException; @Slf4j @Configuration public class MasterDataSourceConfig extends DataSourceConfig{ @Resource private MasterDataSourceProperties dataSourceProperties; /** * 数据源 */ @Bean(name = "masterDataSource") @Primary public DataSource masterDataSource() throws SQLException { return initDruidDataSource(dataSourceProperties); } }
7.从数据库配置
package com.jun.webpro.common.config.dataSource.config; import com.jun.webpro.common.config.dataSource.properties.MasterDataSourceProperties; import com.jun.webpro.common.config.dataSource.properties.SlaveDataSourceProperties; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.annotation.Resource; import javax.sql.DataSource; import java.sql.SQLException; @Slf4j @Configuration public class SlaveDataSourceConfig extends DataSourceConfig{ @Resource private SlaveDataSourceProperties dataSourceProperties; /** * 数据源 */ @Bean(name = "slaveDataSource") public DataSource masterDataSource() throws SQLException { return initDruidDataSource(dataSourceProperties); } }
8.mybatis配置项
这里用于mysql事务
package com.jun.webpro.common.config.dataSource.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisConfig { @Bean("mybatis-config") @ConfigurationProperties(prefix = "mybatis.configuration") public org.apache.ibatis.session.Configuration globalConfiguration() { return new org.apache.ibatis.session.Configuration(); } }
9.将数据源的key设置进ThreadLocal中
这里主要是注解中,将值添加进来,然后后面使用。
package com.jun.webpro.common.config.dataSource.route; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Description 这里切换读/写模式 * 原理是利用ThreadLocal保存当前线程是否处于读模式(通过开始READ_ONLY注解在开始操作前设置模式为读模式, * 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式 */ @Slf4j public class DbContextHolder { public static final String MASTER = "masterDataSource"; public static final String SLAVE = "slaveDataSource"; private static ThreadLocal<String> contextHolder= new ThreadLocal<>(); public static void setDbType(String dbType) { if (dbType == null) { log.error("dbType为空"); throw new NullPointerException(); } log.info("设置dbType为:{}",dbType); contextHolder.set(dbType); } public static String getDbType() { return contextHolder.get(); } public static void clearDbType() { contextHolder.remove(); } }
10.去ThreadLocal中获取荣国注解加入的key
这个key可以决定走哪个库。
package com.jun.webpro.common.config.dataSource.route; import com.jun.webpro.common.config.dataSource.route.DbContextHolder; import com.jun.webpro.common.units.NumberUtils; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import java.util.Objects; /** * Description * */ @Slf4j public class RoutingDataSource extends AbstractRoutingDataSource { @Value("${mysql.datasource.num}") private int num; @Override protected Object determineCurrentLookupKey() { String typeKey = DbContextHolder.getDbType(); if (Objects.equals(DbContextHolder.MASTER, typeKey)) { log.info("使用了写库"); return typeKey; }else { int sum = NumberUtils.getRandom(1, num); log.info("使用了读库{}", sum); return DbContextHolder.SLAVE + sum; } } }
11.读写配置,主要点是重写routingDataSource
package com.jun.webpro.common.config.dataSource.route; import com.jun.webpro.common.config.dataSource.config.DataSourceConfig; import com.jun.webpro.common.config.dataSource.route.DbContextHolder; import com.jun.webpro.common.config.dataSource.route.RoutingDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * Description * */ @Configuration @MapperScan(basePackages = "com.jun.webpro.common.domain.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class WriteOrReadDatabaseConfig extends DataSourceConfig { @Resource private DataSource masterDataSource; @Resource private DataSource slaveDataSource; /** * 事务 */ @Bean public DataSourceTransactionManager masterTransactionManager() { return new DataSourceTransactionManager(routingDataSource()); } @Bean public SqlSessionFactory sqlSessionFactory(@Qualifier("mybatis-config") org.apache.ibatis.session.Configuration configuration) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(routingDataSource()); sessionFactory.setConfiguration(configuration); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); return sessionFactory.getObject(); } /** * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源 * 工厂模式 */ @Bean public AbstractRoutingDataSource routingDataSource() { RoutingDataSource proxy = new RoutingDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(2); targetDataSources.put(DbContextHolder.MASTER, masterDataSource); targetDataSources.put(DbContextHolder.SLAVE+"1", slaveDataSource); proxy.setDefaultTargetDataSource(slaveDataSource); proxy.setTargetDataSources(targetDataSources); return proxy; } }
12.注解
package com.jun.webpro.common.aspect; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Description 通过该接口注释的service使用读模式,其他使用写模式 * * 接口注释只是一种办法,如果项目已经有代码了,通过注释可以不修改任何业务代码加持读写分离 * 也可以通过切面根据方法开头来设置读写模式,例如getXXX()使用读模式,其他使用写模式 * */ @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ReadOnly { }
13,注解实现
将要使用的库写进去
package com.jun.webpro.common.aspect.impl; import com.jun.webpro.common.aspect.ReadOnly; import com.jun.webpro.common.config.dataSource.route.DbContextHolder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; /** * Description * */ @Aspect @Component public class ReadOnlyInterceptor implements Ordered { private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class); @Around("@annotation(readOnly)") public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable{ try{ // 通过注解,将值注册进去 DbContextHolder.setDbType(DbContextHolder.SLAVE); return joinPoint.proceed(); }finally { DbContextHolder.clearDbType(); log.info("清除threadLocal"); } } @Override public int getOrder() { return 0; } }
三:配置项
1.
mysql: datasource: #读库数目 num: 1 master: url: jdbc:mysql://47.103.25.1:3306/center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://47.103.25.1:3306/center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver spring: # Redis配置, 使用了连接池 redis: database: 0 host: 106.14.25.1 port: 6379 password: jedis: pool: max-active: 20 max-wait: -1 max-idle: 20 min-idle: 10 timeout: 1000 #druid数据库连接池配置 druid: initialSize: 5 minIdle: 5 maxActive: 8 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 connectionInitSqls: set names utf8mb4 filters: stat connectionProperties: druid: stat: mergeSql: true slowSqlMillis: 1000
四:关于事务
1.DataSourceTranceManager
AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。
spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。
将事务管理在数据持久 (Dao层) 开启,切换数据源的操作放在业务层进行操作,就可在事务开启之前顺利进行数据源切换,不会再出现切换失败了。
五:缺点
1.不能动态的增加数据源
2.主从延迟问题
如果往主库增加数据,马上到从库读取,一般没有问题
但是数据量大的时候,因为有延迟,可能去查询的时候,查询不到
以上是关于读写分离注解的主要内容,如果未能解决你的问题,请参考以下文章