當項目愈來愈大和併發越來大的狀況下,單個數據庫服務器的壓力確定也是愈來愈大,最終演變成數據庫成爲性能的瓶頸,並且當數據愈來愈多時,查詢也更加耗費時間,固然數據庫數據過大時,能夠採用數據庫分庫分表,同時數據庫壓力過大時,也能夠採用Redis等緩存技術來下降壓力,可是任何一種技術都不是萬金油,不少時候都是經過多種技術搭配使用,而本文主要就是介紹經過讀寫分離來加快數據庫讀取速度php
讀寫分離實現的方式有多種,可是多種都須要配置數據庫的主從複製,固然也許是有不須要配置的,只是我不知道而已java
數據庫中間件實現,如Mycat等數據庫中間件,對於項目自己來講,只有一個數據源,就是連接到Mycat,再由mycat根據規則去選擇從哪一個庫獲取數據mysql
代碼中配置多數據源,經過代碼控制使用哪一個數據源,本文也是主要介紹這種方式git
1.下降數據庫讀取壓力,尤爲是有些須要大量計算的實時報表類應用
2.加強數據安全性,讀寫分離有個好處就是數據近乎實時備份,一旦某臺服務器硬盤發生了損壞,從庫的數據能夠無限接近主庫
3.能夠實現高可用,固然只是配置了讀寫分離並不能實現搞可用,最多就是在Master(主庫)宕機了還能進行查詢操做,具體高可用還須要其餘操做spring
1.增大成本,一臺數據庫服務器和多臺數據庫的成本確定是不同的
2.增大代碼複雜度,不過這點還比較輕微吧,可是也的確會必定程度上加劇
3.增大寫入成本,雖然下降了讀取成本,可是寫入成本倒是一點也沒有下降,畢竟還有從庫一直在向主庫請求數據sql
MySQL主從配置是實現讀寫分離的基本條件,具體實現MySQL主從複製能夠參考我以前的文章MySQL主從複製搭建,基於日誌(binlog)數據庫
spring: application: name: separate master: url: jdbc:mysql://192.168.1.126:3307/test?useUnicode=true&characterEncoding=utf8&emptyStringsConvertToZero=true username: root password: 123456 driver_class_namel: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource max-active: 20 initial-size: 1 min-idle: 3 max-wait: 600 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 test-while-idle: true test-on-borrow: false test-on-return: false poolPreparedStatements: true slave: url: jdbc:mysql://192.168.1.126:3309/test?useUnicode=true&characterEncoding=utf8&emptyStringsConvertToZero=true username: test password: 123456 driver_class_namel: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource max-active: 20 initial-size: 1 min-idle: 3 max-wait: 600 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 test-while-idle: true test-on-borrow: false test-on-return: false poolPreparedStatements: true
文件中配置了2個數據源,master是寫庫,slave是讀庫,爲了防止向slave寫入,slave的用戶只有讀取權限 由於代碼中須要動態的設置數據源,因此數據源須要經過繼承AbstractRoutingDataSource緩存
/** * 動態數據源 * @author Raye * @since 2016年10月25日15:20:40 */ public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<DatabaseType>(); @Override protected Object determineCurrentLookupKey() { return contextHolder.get(); } public static enum DatabaseType { Master, Slave } public static void master(){ contextHolder.set(DatabaseType.Master); } public static void slave(){ contextHolder.set(DatabaseType.Slave); } public static void setDatabaseType(DatabaseType type) { contextHolder.set(type); } public static DatabaseType getType(){ return contextHolder.get(); } }
contextHolder 是線程變量,由於每一個請求是一個線程,因此經過這樣來區分使用哪一個庫
determineCurrentLookupKey是重寫的AbstractRoutingDataSource的方法,主要是肯定當前應該使用哪一個數據源的key,由於AbstractRoutingDataSource 中保存的多個數據源是經過Map的方式保存的安全
/** * Druid的DataResource配置類 * @author Raye * @since 2016年10月7日14:14:18 */ @Configuration @EnableTransactionManagement public class DataBaseConfiguration implements EnvironmentAware { private RelaxedPropertyResolver propertyResolver1; private RelaxedPropertyResolver propertyResolver2; public DataBaseConfiguration(){ System.out.println("#################### DataBaseConfiguration"); } public void setEnvironment(Environment env) { this.propertyResolver1 = new RelaxedPropertyResolver(env, "spring.master."); this.propertyResolver2 = new RelaxedPropertyResolver(env, "spring.slave."); } public DataSource master() { System.out.println("注入Master druid!!!"); DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(propertyResolver1.getProperty("url")); datasource.setDriverClassName(propertyResolver1.getProperty("driver-class-name")); datasource.setUsername(propertyResolver1.getProperty("username")); datasource.setPassword(propertyResolver1.getProperty("password")); datasource.setInitialSize(Integer.valueOf(propertyResolver1.getProperty("initial-size"))); datasource.setMinIdle(Integer.valueOf(propertyResolver1.getProperty("min-idle"))); datasource.setMaxWait(Long.valueOf(propertyResolver1.getProperty("max-wait"))); datasource.setMaxActive(Integer.valueOf(propertyResolver1.getProperty("max-active"))); datasource.setMinEvictableIdleTimeMillis(Long.valueOf(propertyResolver1.getProperty("min-evictable-idle-time-millis"))); try { datasource.setFilters("stat,wall"); } catch (SQLException e) { e.printStackTrace(); } return datasource; } public DataSource slave() { System.out.println("Slave druid!!!"); DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(propertyResolver2.getProperty("url")); datasource.setDriverClassName(propertyResolver2.getProperty("driver-class-name")); datasource.setUsername(propertyResolver2.getProperty("username")); datasource.setPassword(propertyResolver2.getProperty("password")); datasource.setInitialSize(Integer.valueOf(propertyResolver2.getProperty("initial-size"))); datasource.setMinIdle(Integer.valueOf(propertyResolver2.getProperty("min-idle"))); datasource.setMaxWait(Long.valueOf(propertyResolver2.getProperty("max-wait"))); datasource.setMaxActive(Integer.valueOf(propertyResolver2.getProperty("max-active"))); datasource.setMinEvictableIdleTimeMillis(Long.valueOf(propertyResolver2.getProperty("min-evictable-idle-time-millis"))); try { datasource.setFilters("stat,wall"); } catch (SQLException e) { e.printStackTrace(); } return datasource; } @Bean public DynamicDataSource dynamicDataSource() { DataSource master = master(); DataSource slave = slave(); Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); targetDataSources.put(DynamicDataSource.DatabaseType.Master, master); targetDataSources.put(DynamicDataSource.DatabaseType.Slave, slave); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法 dataSource.setDefaultTargetDataSource(master); return dataSource; } }
一共有3個數據源,一個master,一個slave,一個是動態數據源,保存在master和slave,爲了防止spring注入異常,因此master和slave都是主動實例化的,並非交給spring管理服務器
dataSource.setDefaultTargetDataSource(master);
是配置的若是沒有配置當前使用哪一個數據源的默認數據源,原本是打算配置slave,可是由於事物問題,因此配置的master
/** * MyBatis的配置類 * * @author Raye * @since 2016年10月7日14:13:39 */ @Configuration @AutoConfigureAfter({ DataBaseConfiguration.class }) @Slf4j public class MybatisConfiguration { @Bean(name = "sqlSessionFactory") @Autowired public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dynamicDataSource); try { SqlSessionFactory session = bean.getObject(); MapperHelper mapperHelper = new MapperHelper(); //特殊配置 Config config = new Config(); //具體支持的參數看後面的文檔 config.setNotEmpty(true); //設置配置 mapperHelper.setConfig(config); // 註冊本身項目中使用的通用Mapper接口,這裏沒有默認值,必須手動註冊 mapperHelper.registerMapper(Mapper.class); //配置完成後,執行下面的操做 mapperHelper.processConfiguration(session.getConfiguration()); return session; } catch (Exception e) { e.printStackTrace(); } return null; } @Bean(name = "sqlSessionTemplate") @Autowired public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } @Bean public MapperScannerConfigurer scannerConfigurer(){ MapperScannerConfigurer configurer = new MapperScannerConfigurer(); configurer.setSqlSessionFactoryBeanName("sqlSessionFactory"); configurer.setSqlSessionTemplateBeanName("sqlSessionTemplate"); configurer.setBasePackage("wang.raye.**.mapper"); configurer.setMarkerInterface(Mapper.class); return configurer; } }
MybatisConfiguration 主要是配置的sqlSessionFactory和sqlSessionTemplate,以及Mybatis的擴展框架Mapper的配置,若是不須要Mapper,能夠不用配置scannerConfigurer
@Configuration @EnableTransactionManagement @Slf4j @AutoConfigureAfter({ MybatisConfiguration.class }) public class TransactionConfiguration extends DataSourceTransactionManagerAutoConfiguration { @Bean @Autowired public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) { log.info("事物配置"); return new DataSourceTransactionManager(dynamicDataSource); } }
事物配置這裏有一個坑就是,一旦開啓了事物,好像就會切換線程執行,因此並不會使用當前配置的數據源,而會取到默認的數據源,因此只能經過將默認數據源設置爲master
/** * 數據源的切入面 * */ @Aspect @Component @Slf4j public class DataSourceAOP { @Before("execution(* wang.raye.separate.service..*.select*(..)) || execution(* wang.raye.separate.service..*.get*(..))") public void setReadDataSourceType() { DynamicDataSource.slave(); log.info("dataSource切換到:slave"); } @Before("execution(* wang.raye.separate.service..*.insert*(..)) || execution(* wang.raye.separate.service..*.update*(..)) || execution(* wang.raye.separate.service..*.delete*(..)) || execution(* wang.raye.separate.service..*.add*(..))") public void setWriteDataSourceType() { DynamicDataSource.master(); log.info("dataSource切換到:master"); } }
這樣的配置是根據方法名來的,能夠根據本身的狀況配置
也可使用註解來主動切換,建立兩個註解類,一個Master,一個Slave Master.class
/** * 使用主庫的註解 */ public @interface Master { }
Slave.class
/** * 使用讀庫的註解 */ public @interface Slave { }
AOP切入修改
/** * 數據源的切入面 * */ @Aspect @Component @Slf4j public class DataSourceAOP { @Before("(@annotation(wang.raye.separate.annotation.Master) || execution(* wang.raye.separate.service..*.insert*(..)) || " + "execution(* wang.raye.separate.service..*.update*(..)) || execution(* wang.raye.separate.service..*.delete*(..)) || " + "execution(* wang.raye.separate.service..*.add*(..))) && !@annotation(wang.raye.separate.annotation.Slave) -") public void setWriteDataSourceType() { DynamicDataSource.master(); log.info("dataSource切換到:master"); } @Before("(@annotation(wang.raye.separate.annotation.Slave) || execution(* wang.raye.separate.service..*.select*(..)) || execution(* wang.raye.separate.service..*.get*(..))) && !@annotation(wang.raye.separate.annotation.Master)") public void setReadDataSourceType() { DynamicDataSource.slave(); log.info("dataSource切換到:slave"); } }
注:這個AOP切入規則只是包含基本的規格,若是要正常使用,須要擴展規則 簡單的service層代碼
/** * 用戶相關業務接口實現類 */ @Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserMapper mapper; @Master @Override public List<User> selectAll() { return mapper.selectAll(); } @Override public boolean addUser(User user) { return mapper.insertSelective(user) > 0; } @Override public boolean updateUser(User user) { return mapper.updateByPrimaryKey(user) > 0; } @Override public boolean deleteByid(int id) { return mapper.deleteByPrimaryKey(id) > 0; } @Transactional(rollbackFor = Exception.class ) @Override public boolean insertAndUpdate(User user){ log.info("當前key:"+ DynamicDataSource.getType().name()); int count = 0; count += mapper.insertSelective(user); user = null; user.getId(); count += mapper.updateByPrimaryKey(user); return count > 1; } }
這裏全部方法會使用master源,若是去掉selectAll的Master註解,那麼selectAll就會使用slave數據源,insertAndUpdate方法主要是測試使用事物的狀況下是不是向Master數據源寫入以及是否正常回滾
具體代碼能夠直接看個人demo項目讀寫分離demo