Mysql读写分离的两种实现对比:Spring+JPA应用层实现 vs Amoeba中间件实现

前段时间看了篇文章,讲Youku网数据库架构的演变,如何从最开始的读写分离,再到垂直分区,最后到水平分片,一步一步慢慢成熟的。看完之后很有冲动抽出一个模型来把这几种技术都实现一下。

     说干就干,首先是读写分离了,我使用的数据库是Mysql,主从数据复制用的是半同步机制(mysql版本必须 5.5以上),具体配置,可以参照这篇文章: http://blog.csdn.net/changerlove/article/details/6167255, 要注意Windows环境下,mysql配置文件为my.ini, linux下才是my.cnf。

     然后主从复制有两类做法,一类就是在应用层去动态切换数据源,最简单的做法就是根据事务类型来做Route,另外一类就是利用所谓的“中间件”,在应用层和数据源之间通过分析SQL来Route,目前可选的“中间件有Mysql自带的Mysql Proxy还有阿里开发的Amoeba,后者据说比Mysql Proxy更稳定,使用也更简单,所以“中间件”做法里我选了第二种来尝试,待会儿会分析Amoeba的优劣,这儿先讲第一种做法,在应用层实现读写分离。

     在例子中有两台Mysql Server,一台Master 负责写,一台Slave负责读,对应的JPA 配置文件如下:

[html] view plain copy

  1. <?xml version="1.0" encoding="UTF-8"?>  

  2. <persistence version="2.0"  

  3.     xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  

  4.     xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">  

  5.     <!-- used to test R/W Splitting in MYSQL, can be routed by transaction   

  6.         type(read only or not) -->  

  7.     <persistence-unit name="MASTER_000" transaction-type="RESOURCE_LOCAL">  

  8.         <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>  

  9.         <exclude-unlisted-classes>false</exclude-unlisted-classes>  

  10.         <properties>  

  11.             <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />  

  12.             <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/rws_db" />  

  13.             <property name="javax.persistence.jdbc.user" value="root" />  

  14.             <property name="javax.persistence.jdbc.password" value="" />  

  15.             <property name="eclipselink.jdbc.read-connections.min"value="1" />  

  16.             <property name="eclipselink.jdbc.write-connections.min"value="1" />  

  17.             <property name="eclipselink.jdbc.batch-writing" value="JDBC" />  

  18.             <property name="eclipselink.logging.level" value="FINE" />  

  19.             <property name="eclipselink.logging.thread" value="false" />  

  20.             <property name="eclipselink.logging.session" value="false" />  

  21.             <property name="eclipselink.logging.exceptions" value="false" />  

  22.             <property name="eclipselink.logging.timestamp" value="false" />  

  23.         </properties>  

  24.     </persistence-unit>  

  25.   

  26.     <persistence-unit name="SLAVE_000" transaction-type="RESOURCE_LOCAL">  

  27.         <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>  

  28.         <exclude-unlisted-classes>false</exclude-unlisted-classes>  

  29.         <properties>  

  30.             <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />  

  31.             <property name="javax.persistence.jdbc.url" value="jdbc:mysql://146.222.51.163:3306/rws_db" />  

  32.             <property name="javax.persistence.jdbc.user" value="zhuga3" />  

  33.             <property name="javax.persistence.jdbc.password" value="Gmail123" />  

  34.             <property name="eclipselink.jdbc.read-connections.min"value="1" />  

  35.             <property name="eclipselink.jdbc.write-connections.min"value="1" />  

  36.             <property name="eclipselink.jdbc.batch-writing" value="JDBC" />  

  37.             <property name="eclipselink.logging.level" value="FINE" />  

  38.             <property name="eclipselink.logging.thread" value="false" />  

  39.             <property name="eclipselink.logging.session" value="false" />  

  40.             <property name="eclipselink.logging.exceptions" value="false" />  

  41.             <property name="eclipselink.logging.timestamp" value="false" />  

  42.         </properties>  

  43.     </persistence-unit>  

  44.   

  45. </persistence>  


然后系统的事务控制,这个是关键,因为在事务初始化的时候要根据事务属性(R/W)来初始化对应的EntityManager,并开启事务,简单起见,例子里使用JPA的Entity Transaction,而不是JTA。

实现第一步,我们得自定义transactionManager来控制事务的开关,回滚以及资源的初始化等等,直接看代码吧:

[java] view plain copy

  1. package util.transaction;  

  2.   

  3. import javax.persistence.EntityManager;  

  4. import javax.persistence.EntityTransaction;  

  5.   

  6. import org.springframework.transaction.TransactionDefinition;  

  7. import org.springframework.transaction.TransactionException;  

  8. import org.springframework.transaction.support.AbstractPlatformTransactionManager;  

  9. import org.springframework.transaction.support.DefaultTransactionStatus;  

  10.   

  11. public class RWSTransactionManager extends AbstractPlatformTransactionManager {  

  12.   

  13.     private static final long serialVersionUID = -1369860968344021154L;  

  14.   

  15.     @Override  

  16.     protected Object doGetTransaction() throws TransactionException {  

  17.         RWSJPATransactionObject rwsTransaction = new RWSJPATransactionObject();  

  18.         EntityManager entityManager = EntityManagerHolder.getInstance()  

  19.                 .initializeResource(definition.isReadOnly());  

  20.         EntityTransaction transaction = entityManager.getTransaction();  

  21.         rwsTransaction.setTransaction(transaction);  

  22.   

  23.         return rwsTransaction;  

  24.     }  

  25.   

  26.     @Override  

  27.     protected void doBegin(Object txObject, TransactionDefinition definition)  

  28.             throws TransactionException {  

  29.         RWSJPATransactionObject rwsTransaction = (RWSJPATransactionObject) txObject;  

  30.           

  31.         if (!definition.isReadOnly()) {  

  32.             rwsTransaction.getTransaction().begin();  

  33.         }  

  34.     }  

  35.   

  36.     @Override  

  37.     protected void doCommit(DefaultTransactionStatus transactionStatus)  

  38.             throws TransactionException {  

  39.         if (transactionStatus.isReadOnly()) {  

  40.             return;  

  41.         }  

  42.   

  43.         RWSJPATransactionObject rwsTransaction = (RWSJPATransactionObject) transactionStatus  

  44.                 .getTransaction();  

  45.         rwsTransaction.getTransaction().commit();  

  46.     }  

  47.   

  48.     @Override  

  49.     protected void doRollback(DefaultTransactionStatus transactionStatus)  

  50.             throws TransactionException {  

  51.         if (transactionStatus.isReadOnly()) {  

  52.             return;  

  53.         }  

  54.   

  55.         RWSJPATransactionObject rwsTransaction = (RWSJPATransactionObject) transactionStatus  

  56.                 .getTransaction();  

  57.         rwsTransaction.getTransaction().rollback();  

  58.     }  

  59.   

  60.     private class RWSJPATransactionObject {  

  61.         private EntityTransaction transaction;  

  62.   

  63.         public EntityTransaction getTransaction() {  

  64.             return transaction;  

  65.         }  

  66.   

  67.         public void setTransaction(EntityTransaction transaction) {  

  68.             this.transaction = transaction;  

  69.         }  

  70.   

  71.         public RWSJPATransactionObject() {  

  72.             super();  

  73.         }  

  74.     }  

  75.   

  76. }  




对应的 EntityManagerHolder 代码如下:

[java] view plain copy

  1. package util.transaction;  

  2.   

  3. import java.util.HashMap;  

  4. import java.util.Map;  

  5. import java.util.Random;  

  6. import javax.persistence.EntityManager;  

  7. import javax.persistence.EntityManagerFactory;  

  8. import javax.persistence.Persistence;  

  9.   

  10. import org.apache.commons.lang.StringUtils;  

  11.   

  12. public class EntityManagerHolder {  

  13.   

  14.     private Random random = new Random();  

  15.     private String PERSISTENCE_UNIT_HEADER_MASTER = "MASTER_";  

  16.     private String PERSISTENCE_UNIT_HEADER_SLAVE = "SLAVE_";  

  17.     private final ThreadLocal<EntityManager> THREAD_LOCAL = new ThreadLocal<EntityManager>();  

  18.     private final Map<String, EntityManagerFactory> RESOURCE = new HashMap<String, EntityManagerFactory>();  

  19.   

  20.     private static EntityManagerHolder emHolder;  

  21.   

  22.     private EntityManagerHolder() {  

  23.     }  

  24.     public static EntityManagerHolder getInstance() {  

  25.         if (null == emHolder) {  

  26.             emHolder = new EntityManagerHolder();  

  27.         }  

  28.         return emHolder;  

  29.     }  

  30.     EntityManager initializeResource(boolean isReadOnly) {  

  31.         // get random persistence unit, dependents on master and slave  

  32.         // quantities  

  33.         // configured in persistence.xml  

  34.         String puSuffix = "";  

  35.         if (isReadOnly) {  

  36.             puSuffix = StringUtils.leftPad(String.valueOf(random.nextInt(1)),  

  37.                     3"0");  

  38.         } else {  

  39.             puSuffix = StringUtils.leftPad(String.valueOf(random.nextInt(1)),  

  40.                     3"0");  

  41.         }  

  42.   

  43.         String persistenceUnit = (isReadOnly ? PERSISTENCE_UNIT_HEADER_SLAVE  

  44.                 : PERSISTENCE_UNIT_HEADER_MASTER) + puSuffix;  

  45.   

  46.         return createEntityManager(persistenceUnit);  

  47.     }  

  48.   

  49.     private EntityManager createEntityManager(String persistenceUnit) {  

  50.         if (null == RESOURCE.get(persistenceUnit)) {  

  51.             EntityManagerFactory entityManagerFactory = Persistence  

  52.                     .createEntityManagerFactory(persistenceUnit);  

  53.             RESOURCE.put(persistenceUnit, entityManagerFactory);  

  54.         }  

  55.         EntityManager entityManager = THREAD_LOCAL.get();  

  56.         if (null == entityManager || !entityManager.isOpen()) {  

  57.             entityManager = RESOURCE.get(persistenceUnit).createEntityManager();  

  58.             THREAD_LOCAL.set(entityManager);  

  59.         }  

  60.         return entityManager;  

  61.     }  

  62.   

  63.     public EntityManager getEntityManager() {  

  64.         EntityManager entityManager = THREAD_LOCAL.get();  

  65.         if (null == entityManager) {  

  66.             throw new IllegalArgumentException();  

  67.         }  

  68.         return entityManager;  

  69.     }  

  70.   

  71.     public void closeEntityManager() {  

  72.         EntityManager entityManager = THREAD_LOCAL.get();  

  73.         THREAD_LOCAL.set(null);  

  74.         if (null != entityManager)  

  75.             entityManager.close();  

  76.     }  

  77.   

  78. }  


可以看到,在getTransaction方法里我们简单new了一个自定义的Object,其实就是一个EntityTransaction holder,关键是在begin(..)方法里,首先会根据事务属性到EntityManagerHolder中进行资源初始化(主要是EMFactory和EM),在initializeResource 方法里面,简单的写了种路由算法,就是Java随机数,生成对应PersistenceUnit Name的后缀,最后根据这个name去生成对应的EMFactory和EM,并且set到Threadlocal的变量中去,之后在Dao层可以直接通过getEntityManager 方法来获取EntityManager.初始化结束之后,对应的事务直接使用EM里面的EntityTransaction来begin,commit和rollback。


事务管理器写好了,我们先看一下相关的Spring配置:

[html] view plain copy

  1. <?xml version="1.0" encoding="UTF-8"?>  

  2. <beans xmlns="http://www.springframework.org/schema/beans"  

  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  

  4.     xmlns:context="http://www.springframework.org/schema/context"   

  5.     xmlns:tx="http://www.springframework.org/schema/tx"  

  6.     xsi:schemaLocation="http://www.springframework.org/schema/beans  

  7.     http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  

  8.     http://www.springframework.org/schema/context   

  9.     http://www.springframework.org/schema/context/spring-context-2.5.xsd  

  10.     http://www.springframework.org/schema/tx  

  11.     http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">  

  12.         <context:annotation-config />  

  13.     <context:component-scan base-package="*" />  

  14.     <bean id="transactionManager" class="util.transaction.RWSTransactionManager"/>  

  15.     <tx:annotation-driven transaction-manager="transactionManager"/>    

  16. </beans>  


最后需要在service上加上事务注解, 指明事务属性:

[java] view plain copy

  1. @Override  

  2. @Transactional(readOnly = false)  

  3. public void add(T object) {  

  4.     getDao().push(object);  

  5. }  

  6.   

  7. @Override  

  8. @Transactional(readOnly = true)  

  9. public List<T> findByExample(T object) {  

  10.     return getDao().findByExample(object);  

  11. }  

测试类:

[java] view plain copy

  1. package test;  

  2.   

  3. import service.EmployeeService;  

  4. import test.mainTest.BaseMainTest;  

  5. import domain.Employee;  

  6.   

  7. public class TestRWS extends BaseMainTest {  

  8.     public static void main(String[] args) {  

  9.         EmployeeService employeeService = (EmployeeService) ctx  

  10.                 .getBean("employeeService");  

  11.         Employee employee = new Employee();  

  12.         employee.setFirstName("Yinkan");  

  13.         employee.setLastName("Zhu");  

  14.   

  15.         employeeService.add(employee);  

  16. //      employeeService.findByExample(employee);  

  17.     }  

  18. }  


当调用EmployeeService里面的Add方法时(事务属性为写事务),从Log里可以看到成功连接到Master Server,并且插入成功,从库里也同步过去了。


Mysql读写分离的两种实现对比:Spring+JPA应用层实现 vs Amoeba中间件实现_第1张图片


当调用EmployeeService里面的FindByExample方法的时候(事务属性为读事务),从Log里可以看到是从Slave Server读取数据:


Mysql读写分离的两种实现对比:Spring+JPA应用层实现 vs Amoeba中间件实现_第2张图片


到这里第一种方法,在应用层来实现读写分离已经结束了,可以看到,通过事务属性来进行数据源切换这种做法比较简单,但是从软件设计的角度来看,事务控制里面耦合了数据源切换的逻辑,下面一种做法,使用Amoeba直接在SQL level做Route,代码的耦合度大大降低,但是也带来了其他问题。

      Amoeba的配置,请参照官方文档: http://docs.hexnova.com/amoeba/

      Amoeba的原理如下图所示,就是一层代理,对APP来说可见的只有一个DataSource,具体的Masters和Slaves可以在Amoeba里配置。

Mysql读写分离的两种实现对比:Spring+JPA应用层实现 vs Amoeba中间件实现_第3张图片

          

 对应应用层JPA的配置如下:

[html] view plain copy

  1. <?xml version="1.0" encoding="UTF-8"?>  

  2. <persistence version="2.0"  

  3. xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  

  4. xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">  

  5.   

  6. <persistence-unit name="proxyPU" transaction-type="RESOURCE_LOCAL">  

  7.      <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>  

  8.      <exclude-unlisted-classes>false</exclude-unlisted-classes>  

  9.      <properties>  

  10.          <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />  

  11.          <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:8066/rws_db" />  

  12.          <property name="javax.persistence.jdbc.user" value="amoebaUser" />           

  13.          <property name="javax.persistence.jdbc.password" value="123" />   

  14.          <property name="eclipselink.jdbc.read-connections.min"value="1" />   

  15.          <property name="eclipselink.jdbc.write-connections.min"value="1" />    

  16.          <property name="eclipselink.jdbc.batch-writing" value="JDBC" />       

  17.          <property name="eclipselink.logging.level" value="FINE" />          

  18.          <property name="eclipselink.logging.thread" value="false" />         

  19.          <property name="eclipselink.logging.session" value="false" />          

  20.          <property name="eclipselink.logging.exceptions" value="false" />       

  21.          <property name="eclipselink.logging.timestamp" value="false" />     

  22.       </properties>      

  23. </persistence-unit>  

  24. </persistence>              


该PersistenceUnit里面用到的Mysql相关信息,配置在Amoeba的amobe.xml文件里,是一个对外的proxy:

[html] view plain copy

  1. <!-- service class must implements com.meidusa.amoeba.service.Service -->  

  2.     <service name="Amoeba for Mysql" class="com.meidusa.amoeba.net.ServerableConnectionManager">  

  3.         <!-- port -->  

  4.         <property name="port">8066</property>             

  5.         <!-- bind ipAddress -->  

  6.         <!--  

  7.         <property name="ipAddress">127.0.0.1</property> 

  8.          -->       

  9.         <property name="manager">${clientConnectioneManager}</property>  

  10.         <property name="connectionFactory">  

  11.             <bean class="com.meidusa.amoeba.mysql.net.MysqlClientConnectionFactory">  

  12.                 <property name="sendBufferSize">128</property>  

  13.                 <property name="receiveBufferSize">64</property>  

  14.             </bean>  

  15.         </property>  

  16.         <property name="authenticator">  

  17.             <bean class="com.meidusa.amoeba.mysql.server.MysqlClientAuthenticator">     

  18.                 <property name="user">amoebaUser</property>       

  19.                 <property name="password">123</property>      

  20.                 <property name="filter">  

  21.                     <bean class="com.meidusa.amoeba.server.IPAccessController">  

  22.                         <property name="ipFile">${amoeba.home}/conf/access_list.conf</property>  

  23.                     </bean>  

  24.                 </property>  

  25.             </bean>  

  26.         </property>  

  27.     </service>  

  28.       

     然后是事务,事务的话可以直接使用Spring自带的JpaTransactionManager来管理,Entitymanager可以在Service层用annotation来注入,具体代码我就不贴了,一切准备好之后先启动Amoeba,我们来跑一下test, 首先是读,发现log如下:


Mysql读写分离的两种实现对比:Spring+JPA应用层实现 vs Amoeba中间件实现_第4张图片


可以看到Database version: 。。mysql-amoeba-proxy。。 说明连接正确,在看结果读数据也的正常的。

接下来就是测试写数据: 跑了一下,报错了:

Internal Exception: java.sql.SQLException: ResultSet is from UPDATE. No Data.
Error Code: 0
Call: SELECT LAST_INSERT_ID()


Check一下生成的SQL,发现insert之后跑了句SELECT LAST_INSERT_ID(), 因为我程序里Entity的主键是Mysql的自增主键,每次插入都会生成这句话用来返回当前ID,而这个ID是跟着Connection走的,也就是说如果该connection里面没有更新,就拿不到值,可是我们的insert和这句select难道不在一个Connection里面?对的,忘了我们用的是“SQL路由器”吗?insert语句开启主库connection,select则走的是从库的connection,所以查询当然是空了,而天真的eclipselink查出resultSet之后直接.next()了,也没判断是否有值,于是就出现这个error code了,当然对于eclipselink,这种check只能说是nice to have的,正常情况下是不会出现这种情况的,connection在获取数据源的时候就已经有了,这里因为我们拿到的其实是个代理。

考虑到Amoeba的实现原理,即"SQL路由",即使Mysql支持Oracle一样的主键生成策略,在这种情况下似乎也不能正常工作。所以得由应用层来生成主键,这又是Effort。。。

另外从Amoeba的文档里看到,他的另一个缺点是目前不支持事务,这个我的理解是跨connection的事务做不到原子性,特别是当规则配置多主情况下,事务无效。

这么看来Amoeba的使用场景确实有限,首先domain层最好不要使用JPA之类的ORM框架,纯JDBC开发更可靠;然后在数据库架构上,一主多从尽量原子化事务。

综合对比一下,应用层实现读写分离比较靠谱。


你可能感兴趣的:(Mysql读写分离的两种实现对比:Spring+JPA应用层实现 vs Amoeba中间件实现)