SQL的读写分离方案,是一种通用的设计,通常可以较好的提升数据库集群的整体负载能力,当一个mysql实例无法支撑客户端的读写负载时,我们首先会想到对数据库进行“读写分离”
1)在数据库的架构层面,我们使用M-S架构模式,即一主多从,Master主要用于处理write、transaction等核心操作,这些操作必须发生在master上,否则将会导致数据一致性问题。对于slaves,通常用于分流read操作,对于那些对数据实时性要求不高、批量读取、SLOW SQL等操作,我们应该将它们分配到slaves中。
2)能进行读写分离的前提,一个非常重要的指标,就是“集群的R:W比较高”,即较多的read、较少的write,如果读写比很低,读写分离所产生的收益,将是比较微弱的。
3)从架构的视角考虑,Master与slave在整个timeline中,所需要处理的物理write操作是相同的,即在master上发生一个write操作,此操作也必将在slaves上发生(可能时间线不一致,但是最终一定会发生,这取决于binlog和mysql IO线程的处理效率)。因此,粗略来讲,slaves所面对的writes负载与master一致。这也限定我们,“读写比较低”的应用场景下,读写分离收益较低的原因。
4)slaves机器的物理性能,应该与master保持一致,甚至可以更好,毕竟,大量的reads操作需要等待slaves去处理,这些load是master所不会面对的。
本文展示了如何在Spring环境中使用JPA实现dataSource的读写分离(本文没有使用JTA事务),这个东西看起来简单,其实实现起来比较蹩脚,与JDBC有很大区别。
1)使用Spring中的AbstractRoutingDataSource,辅助程序在运行时选择合适的dataSource。
2)可以使用@Master、@Slave注释来强制dao方法调用必须使用master或者slave的数据库源。
3)本例提供的ReadWriteDataSourceRouter可以根据当前Transaction的readOnly特性,将SQL调用按需分发给master或者slaves;可以指定多个slaves,可以简单的负载均衡。
1、persistence.xml
如果我们不适用JTA事务的话,这个文件可以为空即可。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<!--
<persistence-unit name="trial" transaction-type="RESOURCE_LOCAL">
</persistence-unit>
-->
</persistence>
2、ReadWriteDataSourceRouter.java
**
* Created by liuguanqing on 16/5/10.
* 全量读写分离
*/
public class ReadWriteDataSourceRouter extends AbstractRoutingDataSource {
private Integer slaves;//slaves的个数
private Random random = new Random();
//如果基于JDK 7+,可以使用ThreadLocalRandom
public void setSlaves(Integer slaves) {
this.slaves = slaves;
}
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if(!isReadOnly) {
return "WRITE";
}
//如果是只读,可以从任意一个slave中执行
return "READ_" + random.nextInt(slaves);
//如果基于JDK 7+
//ThreadLocalRandom random = ThreadLocalRandom.current();
}
}
java类中使用了一些约定的字符串,比如“WRITE”对应的为masterDataSource,所有的slaves对应的key必须为“READ_” + 数字。(参见下文配置)
3、spring-datasource.xml配置摘要:
<bean id="masterDataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://192.168.1.100:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=false"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="maxActive" value="128"></property>
<property name="maxIdle" value="6"></property>
<property name="minIdle" value="2"></property>
<property name="maxWait" value="30000"></property>
<property name="defaultAutoCommit" value="true"></property>
</bean>
<bean id="slaveDataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://192.168.1.101:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=false"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="maxActive" value="128"></property>
<property name="maxIdle" value="6"></property>
<property name="minIdle" value="2"></property>
<property name="maxWait" value="30000"></property>
<property name="defaultAutoCommit" value="true"></property>
</bean>
<bean id="rwDataSource" class="com.test.demo.dataSource.TypedReadWriteDataSourceRouter">
<property name="slaves" value="2"/><!-- 允许read操作的节点个数 -->
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="WRITE" value-ref="masterDataSource"/>
<entry key="READ_0" value-ref="slaveDataSource" />
<entry key="READ_1" value-ref="masterDataSource" /><!-- 允许部分read到slave上 -->
</map>
</property>
<property name="defaultTargetDataSource" ref="slaveDataSource"/><!-- or master -->
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="classpath:persistence.xml" />
<property name="dataSource" ref="rwDataSource" />
<!-- model的package-->
<property name="packagesToScan" value="com.test.demo.model"/>
<property name="jpaVendorAdapter">
<!-- JPA的实现,有多种,请根据实际情况选择 -->
<bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
<property name="showSql" value="false"/>
<property name="generateDdl" value="false"/>
<property name="database" value="MYSQL"/>
<property name="databasePlatform" value="org.eclipse.persistence.platform.database.MySQLPlatform" />
</bean>
</property>
<property name="jpaProperties">
<props>
<prop key="eclipselink.weaving">false</prop>
<prop key="eclipselink.cache.shared.default">false</prop>
<prop key="eclipselink.read-only">true</prop>
</props>
</property>
</bean>
<!-- 声明一个Spring提供的JPA事务管理器,传入的参数是Spring中的实体管理器工厂 -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<context:annotation-config/>
<tx:annotation-driven transaction-manager="transactionManager" />
4、TesUser.java(model样例)
@Entity
@Table(name="test_user",schema = "vipkid")
public class TestUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@Column(name = "name")
private String name;
@Column(name = "password")
private String password;
// 创建时间
@Temporal(TemporalType.DATE)
@Column(name = "created")
private Date created;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
5、TestUserRepository.java(DAO层)
@Repository
public class TestUserRepository {
@PersistenceContext
private EntityManager entityManager;
@Transactional(readOnly = true)
//To slave
public TestUser getFromSlave(int id) {
String sql = "select T from TestUser T where T.id = :id";
TypedQuery<TestUser> query = entityManager.createQuery(sql,TestUser.class);
query.setParameter("id",id);
return query.getSingleResult();
}
@Transactional(readOnly = false)
//To master
public TestUser getFromMaster(int id) {
String sql = "select T from TestUser T where T.id = :id";
TypedQuery<TestUser> query = entityManager.createQuery(sql,TestUser.class);
query.setParameter("id",id);
return query.getSingleResult();
}
}