数据库写入效率要低于读取效率,一般系统中数据读取频率高于写入频率,单个数据库实例在写入的时候会影响读取性能,这是做读写分离的原因。
实现方式主要基于mysql的主从复制,通过路由的方式使应用对数据库的写请求只在master上进行,读请求在slave上进行。
mysql主从复制:https://www.jianshu.com/p/a68551347d7d
路由的方式主要有两种:
1.代理
在应用和数据库之间增加代理层,代理层接收应用对数据库的请求,根据不同请求类型转发到不同的实例,在实现读写分离的同时可以实现负载均衡。
目前常用的mysql的读写分离中间件有amoeba,MySQL-Proxy
2.应用内路由
在应用程序中实现,针对不同的请求类型去不同的实例执行sql
本文主要介绍第二种方式。基于springboot、 mybatis实现。
思路:之前在做项目的时候实现过mybatis数据源的动态切换。基于原来的方案,用aop来拦截dao层方法,根据方法名称就可以判断要执行的sql类型,动态切换主从数据源。
1.mybatis和数据源配置
2.数据源切换
切换数据源需要用到类AbstractRoutingDataSource
targetDataSources用一个map来存储配置的数据源,defaultTargetDataSource默认的数据源
项目启动时targetDataSources中的值会放到resolvedDataSources,key默认为targetDataSources中的key,可以实现resolveSpecifiedLookupKey()方法处理。
resolvedDefaultDataSource会被赋值给defaultTargetDataSource,因此如果defaultTargetDataSource没有配启动会报错 。
在需要与mysql交互时检索resolvedDataSources中的数据源,通过抽象determineCurrentLookupKey()获取当前数据源的key,因此实现这个方法可以实现数据源的切换。
数据源加载:
/** * Title:MybatisConfiguration * * @author angla **/ @Configuration public class MybatisConfiguration { @Autowired private Environment env; /** * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称) */ @Bean public DataSource masterDataSource() throws Exception { Properties props = new Properties(); props.put("driverClassName", env.getProperty("spring.mastersource.driver-class-name")); props.put("url", env.getProperty("spring.mastersource.url")); props.put("username", env.getProperty("spring.mastersource.username")); props.put("password", env.getProperty("spring.mastersource.password")); return DruidDataSourceFactory.createDataSource(props); } @Bean public DataSource slaveDataSource() throws Exception { Properties props = new Properties(); props.put("driverClassName", env.getProperty("spring.slavesource1.driver-class-name")); props.put("url", env.getProperty("spring.slavesource1.url")); props.put("username", env.getProperty("spring.slavesource1.username")); props.put("password", env.getProperty("spring.slavesource1.password")); return DruidDataSourceFactory.createDataSource(props); } /** * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错 * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例) */ @Bean @Primary @DependsOn({"masterDataSource","slaveDataSource"}) public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_MASTER.getName(), masterDataSource); targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_SLAVE.getName(),slaveDataSource); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法 dataSource.setDefaultTargetDataSource(slaveDataSource);// 默认的datasource设置为myTestDbDataSource return dataSource; } /** * 根据数据源创建SqlSessionFactory */ @Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception { SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); fb.setDataSource(ds);// 指定数据源 fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包 fb.setMapperLocations( new PathMatchingResourcePatternResolver().getResources(Objects.requireNonNull(env.getProperty( "mybatis.mapperLocations")))); return fb.getObject(); } /** * 配置事务管理器 */ @Bean public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception { return new DataSourceTransactionManager(dataSource); } } 数据源枚举: /** * Title:DataSourceTypeEnum * * @author angla **/ public enum DataSourceTypeEnum { DATA_SOURCE_MASTER(1,"master"), DATA_SOURCE_SLAVE(2,"slave"); DataSourceTypeEnum(Integer code, String name) { this.code = code; this.name = name; } private Integer code; private String name; public Integer getCode() { return code; } public String getName() { return name; } }
定义ThreadLocal存储
/** * Title:DataSourceContextHolder * * @author angla **/ public class DataSourceContextHolder { private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>(); public static void setDatabaseType(DataSourceTypeEnum databaseType) { contextHolder.set(databaseType); } public static DataSourceTypeEnum getDatabaseType() { return contextHolder.get(); } } 实现determineCurrentLookupKey方法 /** * Title:DynamicDataSource * * @author angla **/ public class DynamicDataSource extends AbstractRoutingDataSource { protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDatabaseType(); } }
定义aop拦截dao层方法:
@Component @Aspect @Slf4j public class DataSourceAspect { private static final String[] queryStrs = {"query", "select", "get"}; /** * 定义切入点,切入点为com.angla.demo.dao下的所有方法 */ @Pointcut("execution(* com.angla.demo.dao.*.*(..))") public void executeSql() { } /** * 前置通知:在连接点之前执行的通知 * * @param joinPoint * @throws Throwable */ @Before("executeSql()") public void doBefore(JoinPoint joinPoint) throws Throwable { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); String mName = methodSignature.getMethod().getName(); log.info("拦截sql方法:{}", mName); DataSourceContextHolder.setDatabaseType(DataSourceTypeEnum.DATA_SOURCE_MASTER); for (String name : queryStrs) { if (mName.startsWith(name)) { log.info("查询语句,设置数据源为slave"); DataSourceContextHolder.setDatabaseType(DataSourceTypeEnum.DATA_SOURCE_SLAVE); break; } } log.info("当前数据源:{}",DataSourceContextHolder.getDatabaseType().getName()); } }
至此,一个简单的读写分离实现就完成了,测试下结果:
停掉master实例,写数据报错,可以正常读取数据,停掉slave实例可以正常写数据,不能读取数据,结果是没问题的。但是这样还不够,现在加载数据源只能加载一主一从,不能适用一主多从或者多主多从的情况,后面需要改下数据源加载和获取方式。
多主多从配置:
加载数据源配置:
@Data @Component @ConfigurationProperties(prefix = "spring") public class DataSourceProperties { private List<Map<String,String>> mastersources; private List<Map<String,String>> slavesources; } @Autowired private DataSourceProperties dataSourceProperties; /** * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称) */ @Bean public List<DataSource> masterDataSources() throws Exception { List<Map<String, String>> mastersources = dataSourceProperties.getMastersources(); if (CollectionUtils.isEmpty(mastersources)) { throw new IllegalArgumentException("需要至少一个主数据源"); } List<DataSource> dataSources = new ArrayList<>(); for (Map map : mastersources) { dataSources.add(DruidDataSourceFactory.createDataSource(map)); } return dataSources; } @Bean public List<DataSource> slaveDataSources() throws Exception { List<Map<String, String>> slavesources = dataSourceProperties.getSlavesources(); if (CollectionUtils.isEmpty(slavesources)) { throw new IllegalArgumentException("需要至少一个从数据源"); } List<DataSource> dataSources = new ArrayList<>(); for (Map map : slavesources) { dataSources.add(DruidDataSourceFactory.createDataSource(map)); } return dataSources; } /** * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错 * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例) */ @Bean @Primary @DependsOn({"masterDataSources", "slaveDataSources"}) public DynamicDataSource dataSource(List<DataSource> masterDataSources, List<DataSource> slaveDataSources) { Map<Object, Object> targetDataSources = new HashMap<>(); for (int i = 0; i < masterDataSources.size(); i++) { targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_MASTER.getName() + i, masterDataSources.get(i)); } for (int i = 0; i < slaveDataSources.size(); i++) { targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_SLAVE.getName() + i, slaveDataSources.get(i)); } DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法 dataSource.setDefaultTargetDataSource(slaveDataSources.get(0));// 默认的datasource设置为myTestDbDataSource return dataSource; }
用随机的方式获取数据源:
@Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { @Autowired private DataSourceProperties dataSourceProperties; protected Object determineCurrentLookupKey() { DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDatabaseType(); int i; List masterSources = dataSourceProperties.getMastersources(); List slaveSources = dataSourceProperties.getSlavesources(); if (dataSourceType.equals(DataSourceTypeEnum.DATA_SOURCE_MASTER)) { i = ThreadLocalRandom.current().nextInt(masterSources.size()) % masterSources.size(); } else { i = ThreadLocalRandom.current().nextInt(slaveSources.size()) % slaveSources.size(); } return dataSourceType.getName() + i; } }
当然数据源加载完成后也可以用其他方式来做多数据源的负载均衡,只需要重写determineCurrentLookupKey()方法就行。