在传统的单机体系中,我们在操作数据库时,只需要直接得到数据库的连接,然后操作数据库即可,可是在现在的数据爆炸时代,只靠单机是无法承载如此大的用户量的,即我们不能纵向扩展,那么我们就只能水平进行扩展,即使用读写分离的主从数据库来缓解数据库的压力,而在读写分离之后,如何使程序能正确的得到主数据库的连接或者是从数据库的连接,就是我们今天读写分离的数据库中间件需要实现的。
一、主从数据库介绍:
主从数据库即为一个主数据库会有对应n个从数据库,而从数据库只能有一个对应的从数据库。主从数据库中写的操作需要使用主数据库,而读操作使用从数据库。主数据库与从数据库始终保持数据一致性。其中保持数据库一致的原理即为当主数据库数据发生变化时,会将操作写入到主数据库日志中,而从数据库会不停的读取主数据库的日志保存到自己的日志系统中,然后进行执行,从而保持了主从数据库一致。
二、开发前准备及ConnectionFactory类的开发:
在了解了主从数据库后,我们可以进行分布式数据库中间件的开发,由于mysql本身支持主从数据库,但限于篇幅,就不讲mysql的主从配置了,我们先使用本机的mysql作为一主两从的数据库源即可,下面是我本机的数据库连接配置文件,其中有一主两从:
- master.driver=com.mysql.jdbc.Driver
- master.dburl=jdbc\:mysql\://127.0.0.1\:3306/master_slave_db
- master.user=root
- master.password=mytestcon
- slave1.driver=com.mysql.jdbc.Driver
- slave1.dburl=jdbc\:mysql\://127.0.0.1\:3306/master_slave_db
- slave1.user=root
- slave1.password=mytestcon
- slave2.driver=com.mysql.jdbc.Driver
- slave2.dburl=jdbc\:mysql\://127.0.0.1\:3306/master_slave_db
- slave2.user=root
- slave2.password=mytestcon
master.driver=com.mysql.jdbc.Drivermaster.dburl=jdbc\:mysql\://127.0.0.1\:3306/master_slave_dbmaster.user=rootmaster.password=mytestconslave1.driver=com.mysql.jdbc.Driverslave1.dburl=jdbc\:mysql\://127.0.0.1\:3306/master_slave_dbslave1.user=rootslave1.password=mytestconslave2.driver=com.mysql.jdbc.Driverslave2.dburl=jdbc\:mysql\://127.0.0.1\:3306/master_slave_dbslave2.user=rootslave2.password=mytestcon
有了主从数据库的连接配置后,就可以将配置进行封装。
封装的DataSource类:
- public class DataSource {
- private String driver;
- private String url;
- private String user;
- private String password;
- public String getDriver() {
- return driver;
- }
- public void setDriver(String driver) {
- this.driver = driver;
- }
- public String getUrl() {
- return url;
- }
- public void setUrl(String url) {
- this.url = url;
- }
- public String getUser() {
- return user;
- }
- public void setUser(String user) {
- this.user = user;
- }
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- }
public class DataSource { private String driver; private String url; private String user; private String password; public String getDriver() { return driver; } public void setDriver(String driver) { this.driver = driver; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }}
在Spring配置文件中,从配置文件中读取配置并将配置转换为封装的DataSource类:
- <bean id="masterDataSource" class="com.happyheng.connection.DataSource">
- <property name="driver" value="${master.driver}"/>
- <property name="url" value="${master.dburl}"/>
- <property name="user" value="${master.user}"/>
- <property name="password" value="${master.password}"/>
- </bean>
- <bean id="slaveDataSource1" class="com.happyheng.connection.DataSource">
- <property name="driver" value="${slave1.driver}"/>
- <property name="url" value="${slave1.dburl}"/>
- <property name="user" value="${slave1.user}"/>
- <property name="password" value="${slave1.password}"/>
- </bean>
- <bean id="slaveDataSource2" class="com.happyheng.connection.DataSource">
- <property name="driver" value="${slave2.driver}"/>
- <property name="url" value="${slave2.dburl}"/>
- <property name="user" value="${slave2.user}"/>
- <property name="password" value="${slave2.password}"/>
- </bean>
- <bean id="propertyConfigurer"
- class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
- <property name="locations">
- <list>
- <value>classpath:dataSource.properties</value>
- </list>
- </property>
- </bean>
classpath:dataSource.properties
有了主从的连接之后,我们就可以写一个ConnectionFactory类,此类可以为外部类直接提供主数据库、从数据库的连接,相当于数据库连接的封装:
- @Service
- public class ConnectionFactory {
- @Autowired
- private DataSource masterDataSource;
- @Autowired
- private DataSource slaveDataSource1;
- @Autowired
- private DataSource slaveDataSource2;
- private List<DataSource> slaveDataSourceList;
- private int slaveDataSourceSize;
- @PostConstruct
- private void init() {
- slaveDataSourceList = new ArrayList<>();
- slaveDataSourceList.add(slaveDataSource1);
- slaveDataSourceList.add(slaveDataSource2);
- slaveDataSourceSize = slaveDataSourceList.size();
- }
- /**
- * 得到主数据的连接
- */
- public Connection getMasterConnection() {
- return getConnection(masterDataSource);
- }
- /**
- * 得到从数据库的连接数量
- */
- public int getSlaveDataSourceSize() {
- return slaveDataSourceSize;
- }
- /**
- * 得到从数据n的连接
- */
- public Connection getSlaveConnection(int index){
- return getConnection(slaveDataSourceList.get(index));
- }
- private Connection getConnection(DataSource dataSource){
- Connection connection = null;
- try {
- Class.forName(dataSource.getDriver());
- connection = DriverManager.getConnection(dataSource.getUrl(), dataSource.getUser(), dataSource.getPassword());
- } catch (Exception e) {
- e.printStackTrace();
- }
- return connection;
- }
- }
@Servicepublic class ConnectionFactory { @Autowired private DataSource masterDataSource; @Autowired private DataSource slaveDataSource1; @Autowired private DataSource slaveDataSource2; private ListslaveDataSourceList; private int slaveDataSourceSize; @PostConstruct private void init() { slaveDataSourceList = new ArrayList<>(); slaveDataSourceList.add(slaveDataSource1); slaveDataSourceList.add(slaveDataSource2); slaveDataSourceSize = slaveDataSourceList.size(); } /** * 得到主数据的连接 */ public Connection getMasterConnection() { return getConnection(masterDataSource); } /** * 得到从数据库的连接数量 */ public int getSlaveDataSourceSize() { return slaveDataSourceSize; } /** * 得到从数据n的连接 */ public Connection getSlaveConnection(int index){ return getConnection(slaveDataSourceList.get(index)); } private Connection getConnection(DataSource dataSource){ Connection connection = null; try { Class.forName(dataSource.getDriver()); connection = DriverManager.getConnection(dataSource.getUrl(), dataSource.getUser(), dataSource.getPassword()); } catch (Exception e) { e.printStackTrace(); } return connection; }}
封装完成后,我们就可以使用getMasterConnection()直接得到主数据库的连接,使用getSlaveConnection(int)可以得到从数据库1或者是从数据2的连接。
三、Proxy代理类的实现:
代理类即是可以让程序中数据库访问得到正确的数据库连接,所以称为代理。
1、使用ThreadLocal为当前线程指定数据库访问模式:
由于Proxy不知道程序使用的是主数据库还是从数据库,所以程序在访问数据库之前要调用Proxy代理类来为当前线程打一个Tag,即指定是使用主数据库还是从数据库。由于而web服务器中每个请求是多线程环境,所以使用ThreadLocal类:
2、使用随机法来访问从数据库:
由于从数据库有多个,所以我们可以使用随机法来随机访问每个从数据库,随机法在高并发的情况下有很平均的分布,性能也非常好。
3、具体实现:
- @Service
- public class DataSourceProxy {
- ThreadLocal<String> dataSourceThreadLocal = new ThreadLocal<>();
- public static final String MASTER = "master";
- public static final String SLAVE = "slave";
- @Resource
- private ConnectionFactory connectionFactory;
- /**
- * 设置当前线程的数据库Mode
- */
- public void setMode(String dataMode) {
- dataSourceThreadLocal.set(dataMode);
- }
- /**
- * 得到当前数据库Mode
- */
- public String getMode() {
- return dataSourceThreadLocal.get();
- }
- /**
- * 根据当前Mode得到Connection连接对象
- */
- public Connection getThreadConnection() {
- // 1.判断当前是从数据还是主数据库,默认是主数据库
- String mode = getMode();
- if (!StringUtils.isEmpty(mode) && SLAVE.equals(mode)) {
- // y1.如果是从数据库,那么使用随机数的形式来得到从数据库连接
- double random = Math.random();
- int index = (int) (random * connectionFactory.getSlaveDataSourceSize());
- System.out.println("----使用的为第" + (index + 1) + "从数据库----");
- return connectionFactory.getSlaveConnection(index);
- } else {
- System.out.println("----使用的为主数据库----");
- // f1.如果是主数据库,因为只有一个,所以直接获取即可
- return connectionFactory.getMasterConnection();
- }
- }
- }
@Servicepublic class DataSourceProxy { ThreadLocaldataSourceThreadLocal = new ThreadLocal<>(); public static final String MASTER = "master"; public static final String SLAVE = "slave"; @Resource private ConnectionFactory connectionFactory; /** * 设置当前线程的数据库Mode */ public void setMode(String dataMode) { dataSourceThreadLocal.set(dataMode); } /** * 得到当前数据库Mode */ public String getMode() { return dataSourceThreadLocal.get(); } /** * 根据当前Mode得到Connection连接对象 */ public Connection getThreadConnection() { // 1.判断当前是从数据还是主数据库,默认是主数据库 String mode = getMode(); if (!StringUtils.isEmpty(mode) && SLAVE.equals(mode)) { // y1.如果是从数据库,那么使用随机数的形式来得到从数据库连接 double random = Math.random(); int index = (int) (random * connectionFactory.getSlaveDataSourceSize()); System.out.println("----使用的为第" + (index + 1) + "从数据库----"); return connectionFactory.getSlaveConnection(index); } else { System.out.println("----使用的为主数据库----"); // f1.如果是主数据库,因为只有一个,所以直接获取即可 return connectionFactory.getMasterConnection(); } }}
4、此工程已在github上开源,可以完整实现数据库的读写分离,地址为: 。如果觉得不错,那么就star一下来鼓励我吧。