|
精华帖 (0) :: 良好帖 (13) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
|---|---|
| 作者 | 正文 |
|
时间:2008-06-19
/**
* 作者:张荣华 * 日期:2008-6-19 **/ 前言: 之前downpour有一个贴(http://www.javaeye.com/topic/143714)讨论了在java中如何使用mysql的master-slave模式(master-slave模式的介绍见Qieqie的这个贴:http://www.javaeye.com/topic/162717),其中readonly大大提到我们可以使用ReplicationDriver来从connection层把read或者write操作分开。这确实是一个比较好的方案,在那个帖子讨论后不久,我就在自己的机器上搭了一个mysql的master-slave模式,然后使用ReplicationDriver来控制读写访问不同的机器,测试通过了,事隔几个月之后,我准备把它用于生产环境中,但是问题来了,因为我的应用访问的数据库有多个,主要访问的库是master-slave模式,其他辅助库是就是指定的一台机器,这时候问题来了。 Mysql的文档是这么写的:ReplicationDriver does not currently work with java.sql.DriverManager -based connection creation unless it is the only MySQL JDBC driver registered with the DriverManager . DriverManager是一个单例模式,一个DriverManager只能注册一个ReplicationDriver驱动,也就是说ReplicationDriver和Driver两个类不能同时使用,郁闷,及其郁闷,由于我之前没有仔细看这段说明,所以没有预料到这种情况。摆在前面的路有几条 一,使用多个datasource解决问题, 二,所有得datasource都使用这个驱动,但是这样做有一个缺点,在文章后面我会详细阐述这种做法得缺点。 三,扩展再扩展,hack再hack。 四,这种方案是第二种方案的补充,详见后文。 首先,我们来看一下ReplicationDriver的官方使用教程: public static void main(String[] args) throws Exception {
ReplicationDriver driver = new ReplicationDriver();
Properties props = new Properties();
// We want this for failover on the slaves
props.put("autoReconnect", "true");
// We want to load balance between the slaves
props.put("roundRobinLoadBalance", "true");
props.put("user", "foo");
props.put("password", "bar");
//
// Looks like a normal MySQL JDBC url, with a
// comma-separated list of hosts, the first
// being the 'master', the rest being any number
// of slaves that the driver will load balance against
//
Connection conn =
driver.connect("jdbc:mysql://master,slave1,slave2,slave3/test",
props);
//
// Perform read/write work on the master
// by setting the read-only flag to "false"
//
//这个节点应该是通过spring的事务管理来设置,同时这个conn对象应该不是一个真正的connection,
//而是一个代理类,通过设置readonly,代理类会去使用不同的connection,
//那么问题是它该代理类使用的connection是哪里取的,抑或说难道它每次都会新开一个connection?,需要看源代码
conn.setReadOnly(false);
conn.setAutoCommit(false);
conn.createStatement().executeUpdate("UPDATE some_table ....");
conn.commit();
//
// Now, do a query from a slave, the driver automatically picks one
// from the list
//
conn.setReadOnly(true);
ResultSet rs =
conn.createStatement().executeQuery("SELECT a,b FROM alt_table");
.......
}
这个示例看上去非常之简单,我们可以很容易的就通过ReplicationDriver拿到了一个Connection,首先,对于我们来说,conn.setReadOnly对我们来说这个方法应该是通过spring的事务管理来设置,同时这个conn对象应该不是一个真正的connection,而是一个代理类,通过设置readonly,代理类会去使用不同的connection,那么问题是它该代理类使用的connection是哪里取的,抑或说难道它每次都会新开一个connection?,这就需要看源代码 那么现在我们要弄清楚ReplicationDriver是怎么回事,反编译之后我们看到: public ReplicationDriver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new NonRegisteringReplicationDriver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
看来看去,这个类中没有什么东西,那么再看看NonRegisteringReplicationDriver类吧。如下面的代码所示,这个类中主要就是这个方法connect方法 public Connection connect(String url, Properties info) throws SQLException {
Properties parsedProps = parseURL(url, info);
if (parsedProps == null) {
return null;
}
Properties masterProps = (Properties) parsedProps.clone();
Properties slavesProps = (Properties) parsedProps.clone();
slavesProps.setProperty("com.mysql.jdbc.ReplicationConnection.isSlave",
"true");
String hostValues = parsedProps.getProperty("HOST");
if (hostValues != null) {
StringTokenizer st = new StringTokenizer(hostValues, ",");
StringBuffer masterHost = new StringBuffer();
StringBuffer slaveHosts = new StringBuffer();
if (st.hasMoreTokens()) {
String hostPortPair[] = parseHostPortPair(st.nextToken());
if (hostPortPair[0] != null) {
masterHost.append(hostPortPair[0]);
}
if (hostPortPair[1] != null) {
masterHost.append(":");
masterHost.append(hostPortPair[1]);
}
}
boolean firstSlaveHost = true;
do {
if (!st.hasMoreTokens()) {
break;
}
String hostPortPair[] = parseHostPortPair(st.nextToken());
if (!firstSlaveHost) {
slaveHosts.append(",");
} else {
firstSlaveHost = false;
}
if (hostPortPair[0] != null) {
slaveHosts.append(hostPortPair[0]);
}
if (hostPortPair[1] != null) {
slaveHosts.append(":");
slaveHosts.append(hostPortPair[1]);
}
} while (true);
if (slaveHosts.length() == 0) {
throw SQLError
.createSQLException(
"Must specify at least one slave host to connect to for master/slave replication "
+ "load-balancing functionality",
"01S00");
}
masterProps.setProperty("HOST", masterHost.toString());
slavesProps.setProperty("HOST", slaveHosts.toString());
}
return new ReplicationConnection(masterProps, slavesProps);
}
上面这个方法也很简单,就是解析url后,然后访问确定master和slave机器一些properties的配置。越来越接近真相了,继续往下看,让我们掀起ReplicationConnection的头盖来: 先看构造方法: public ReplicationConnection(Properties masterProperties,
Properties slaveProperties) throws SQLException {
Driver driver = new Driver();
StringBuffer masterUrl = new StringBuffer("jdbc:mysql://");
StringBuffer slaveUrl = new StringBuffer("jdbc:mysql://");
String masterHost = masterProperties.getProperty("HOST");
if (masterHost != null) {
masterUrl.append(masterHost);
}
String slaveHost = slaveProperties.getProperty("HOST");
if (slaveHost != null) {
slaveUrl.append(slaveHost);
}
String masterDb = masterProperties.getProperty("DBNAME");
masterUrl.append("/");
if (masterDb != null) {
masterUrl.append(masterDb);
}
String slaveDb = slaveProperties.getProperty("DBNAME");
slaveUrl.append("/");
if (slaveDb != null) {
slaveUrl.append(slaveDb);
}
//从这里可以看出,笔者前文提出的猜想是正确的,每一个ReplicationDriver其实是两个Connection的代理,这两个
//Connection才是真正访问DB的connection。 masterConnection = (com.mysql.jdbc.Connection) driver.connect(masterUrl
.toString(), masterProperties);
slavesConnection = (com.mysql.jdbc.Connection) driver.connect(slaveUrl
.toString(), slaveProperties);
currentConnection = masterConnection;
}
这个构造方法没有任何的玄机,从这里也可以看出,那么前文提出的猜想是正确的,每一个ReplicationDriver其实是两个Connection的代理,这两个Connection才是真正访问DB的connection。好了,看到这里看客们大概也看出来了,当调用connection.setReadonly的时候,其实就是把需要的masterConnection或者slavesConnection赋值给当前的currentConnection,ReplicationDriver就是这么个实现,原理也非常简单,那么怎么解决文章中开头提出的那个问题呢。 第一种方案: 改成多个datasource,这种方式是最简单,最粗鲁的,然后我们就可以看到一堆有一堆,一坨又一坨的datasource,然后你还有一堆堆一坨坨的JdbcTemplate,HibernateTemplate,SqlMapClientTemplate,等等。 第二种方案: 第二种方案是所有的驱动都是用ReplicationDriver,有同学问:那怎么行呢,因为我又的datasource不是master-slave模式的。还好,没有什么关系,即使是这样配置jdbc:mysql://192.168.1.1:3306,192.168.1.1:3306/xx也是没有关系的,带来的结果就是一个ReplicationDriver其实hold了两个connection,而这两个connection其实是连着同一个数据库。那么也就是说如果连接池里配置了50个connection,那么实际上却有100个connection连着数据库,这种事情还是比较让人郁闷的。 第三种方案: 看来看去,问题都出现在DriverManager上,如果我新建一个DriverManager,行否,于是新建一个类,名约ReplicationDriverManager。这样系统中就有两个DriverManager了,普通的DriverManager注册的驱动为Driver.java,ReplicationDriverManager注册的驱动为ReplicationDriver。大家互不干扰,貌似可行。粗略的看了一下代码,也是可以实现的,关键在于需要扩展连接池(至少c3p0是这样的,需要重写c3p0的两个类),然后还需要重写一个ReplicationDriver,将静态块中的DriverManager换成我们自己的DriverManager。然后还需要重写ReplicationConnection,Driver类等等,也是非常麻烦的。 想来想去,想破了头了,终于,还是有点头绪,就是在第二种方案的基础上,再次修改ReplicationConnection,也就是说,如果我的配置为jdbc:mysql://192.168.1.1:3306/xx,那么我强行把currentConnection设置为masterConnection,这样ReplicationConnection中的slavesConnection就一直是空着的,或者masterConnection和slavesConnection还有currentConnection这3个引用都指向同一个对象,那么连接池中配置50个连接,那么就是50个连接,不会变成100个连接了,而其他的master-slave模式的配置依旧,这个方式貌似看上去还是不错的。我们看看代码怎么写: 首先,来一个EasyReplicationDriver,代码如下: public class EasyReplicationDriver extends EasyNonRegisteringReplicationDriver
implements Driver {
public EasyReplicationDriver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new EasyNonRegisteringReplicationDriver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}
接着再来一个EasyNonRegisteringReplicationDriver,如下: /**
* @author ahuaxuan(aaron zhang)
* @since 2008-6-18
* @version $Id$
*/
public class EasyNonRegisteringReplicationDriver extends NonRegisteringDriver {
public EasyNonRegisteringReplicationDriver() throws SQLException {
}
public Connection connect(String url, Properties info) throws SQLException {
Properties parsedProps = parseURL(url, info);
if (parsedProps == null) {
return null;
}
Properties masterProps = (Properties) parsedProps.clone();
Properties slavesProps = (Properties) parsedProps.clone();
slavesProps.setProperty("com.mysql.jdbc.ReplicationConnection.isSlave",
"true");
String hostValues = parsedProps.getProperty("HOST");
if (hostValues != null) {
StringTokenizer st = new StringTokenizer(hostValues, ",");
StringBuffer masterHost = new StringBuffer();
StringBuffer slaveHosts = new StringBuffer();
if (st.hasMoreTokens()) {
String hostPortPair[] = parseHostPortPair(st.nextToken());
if (hostPortPair[0] != null) {
masterHost.append(hostPortPair[0]);
}
if (hostPortPair[1] != null) {
masterHost.append(":");
masterHost.append(hostPortPair[1]);
}
}
boolean firstSlaveHost = true;
do {
if (!st.hasMoreTokens()) {
break;
}
String hostPortPair[] = parseHostPortPair(st.nextToken());
if (!firstSlaveHost) {
slaveHosts.append(",");
} else {
firstSlaveHost = false;
}
if (hostPortPair[0] != null) {
slaveHosts.append(hostPortPair[0]);
}
if (hostPortPair[1] != null) {
slaveHosts.append(":");
slaveHosts.append(hostPortPair[1]);
}
} while (true);
/*if (slaveHosts.length() == 0) {
throw SQLError
.createSQLException(
"Must specify at least one slave host to connect to for master/slave replication "
+ "load-balancing functionality",
"01S00");
}*/
masterProps.setProperty("HOST", masterHost.toString());
slavesProps.setProperty("HOST", slaveHosts.toString());
}
return new EasyReplicationConnection(masterProps, slavesProps);
}
注意上面我注释掉的这段代码,如果我们想要ReplicationDriver支持jdbc:mysql://192.168.1.1:3306/xxx,那么就必须把上面那段代码注释掉。 第3步,让我们看看EasyReplicationConnection这个类: public EasyReplicationConnection(Properties masterProperties,
Properties slaveProperties) throws SQLException {
Driver driver = new Driver();
StringBuffer masterUrl = new StringBuffer("jdbc:mysql://");
StringBuffer slaveUrl = new StringBuffer("jdbc:mysql://");
String masterHost = masterProperties.getProperty("HOST");
if (masterHost != null) {
masterUrl.append(masterHost);
}
String slaveHost = slaveProperties.getProperty("HOST");
if (slaveHost != null) {
slaveUrl.append(slaveHost);
}
String masterDb = masterProperties.getProperty("DBNAME");
masterUrl.append("/");
if (masterDb != null) {
masterUrl.append(masterDb);
}
String slaveDb = slaveProperties.getProperty("DBNAME");
slaveUrl.append("/");
if (slaveDb != null) {
slaveUrl.append(slaveDb);
}
//从这里可以看出,笔者前文提出的猜想是正确的,每一个ReplicationDriver其实是两个Connection的代理,这两个
//Connection才是真正访问DB的connection。
masterConnection = (com.mysql.jdbc.Connection) driver.connect(masterUrl
.toString(), masterProperties);
if (slaveUrl.toString().contains("///")) {
if (logger.isDebugEnabled()) {
logger.debug(" ----- the salveUrl contains the '///', " +
"that means there is no slaver, make slavesConnection = masterConnection --");
}
slavesConnection = masterConnection;
} else {
slavesConnection = (com.mysql.jdbc.Connection) driver.connect(slaveUrl
.toString(), slaveProperties);
}
主要就是加了一个判断,一旦路径中出现///,那么就证明没有slave机器,那么就可以把masterConnection赋值给slavesConnection了。这样一来就ok了。 经过3个类的改写之后,终于,我们可以使用ReplicationDriver的功能了,看来看去还是这种方式最美好。 不过由于ahuaxuan的水平所限,可能在以上的方案中有其没有发现的问题,抑或有更好的方案,希望大家不吝赐教。 声明:JavaEye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
|
|
| 返回顶楼 | |
|
时间:2008-06-20
ahuaxuan 写道 我准备把它用于生产环境中,但是问题来了,因为我的应用访问的数据库有多个,主要访问的库是master-slave模式,其他辅助库是就是指定的一台机器,这时候问题来了。
没明白这段的意思,你有应用要访问N个数据库,但是其中只有一个数据库需要ReplicationDriver? 既然这样的话,给这个数据库定义的datasource设置driver class name为ReplicationDriver,而其他的都设置成普通的mysql driver不可以么? |
|
| 返回顶楼 | |
|
时间:2008-06-20
Readonly 写道 ahuaxuan 写道 我准备把它用于生产环境中,但是问题来了,因为我的应用访问的数据库有多个,主要访问的库是master-slave模式,其他辅助库是就是指定的一台机器,这时候问题来了。
没明白这段的意思,你有应用要访问N个数据库,但是其中只有一个数据库需要ReplicationDriver? 既然这样的话,给这个数据库定义的datasource设置driver class name为ReplicationDriver,而其他的都设置成普通的mysql driver不可以么? 我开始也是这样做的,但是后来发现问题了,因为一个DriverManager只能有一个ReplicationDriver驱动,不能在使用其他驱动了 它的文档写道:ReplicationDriver does not currently work with java.sql.DriverManager -based connection creation unless it is the only MySQL JDBC driver registered with the DriverManager . 所以我就没有办法了,才有了这个扩展 |
|
| 返回顶楼 | |
|
时间:2008-06-20
实践出真知,看来很多东西不去实践是不清楚的。
|
|
| 返回顶楼 | |
|
时间:2008-06-20
明白你的意思了,因为这2个driver都用一样的url协议头jdbc:mysql://,所以就不能一起注册了。
对于你这种情况,你可用普通的driver class name,用不同的协议头: 1. jdbc:mysql:replication:// 2. jdbc:mysql:// |
|
| 返回顶楼 | |
|
时间:2008-06-20
Readonly 写道 明白你的意思了,因为这2个driver都用一样的url协议头jdbc:mysql://,所以就不能一起注册了。
对于你这种情况,你可用普通的driver class name,用不同的协议头: 1. jdbc:mysql:replication:// 2. jdbc:mysql:// 刚才试了一下,好像不行,我是这样做的, jdbc:mysql:replication://192.168.1.1,192.168.1.1/xx 这个地址用ReplicationDriver作为驱动 jdbc:mysql://192.168.1.2/xxx 这个地址用Driver类作为驱动。 |
|
| 返回顶楼 | |
|
时间:2008-06-20
偶只用过单纯的ReplicationDriver,不过从源代码看jdbc:mysql:replication://这样是可用的,你遇到具体出错信息是啥?
|
|
| 返回顶楼 | |
|
时间:2008-06-20
Readonly 写道 偶只用过单纯的ReplicationDriver,不过从源代码看jdbc:mysql:replication://这样是可用的,你遇到具体出错信息是啥?
这个是具体的错误信息: 14:31:41,713 WARN BasicResourcePool:1841 - com.mchange.v2.resourcepool.BasicResourcePool$AcquireTask@16fc6b62 -- Acquisition Attempt Failed!!! Clearing pending acquires. While trying to acquire a needed new resource, we failed to succeed more than the maximum number of allowed acquisition attempts (30). Last acquisition attempt exception: java.sql.SQLException: Must specify at least one slave host to connect to for master/slave replication load-balancing functionality at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1056) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:957) 刚才downpour提出不需要使用ReplicationDriver,只需要指定不同的协议即可,我试了一下,过了 也就是说这样就可以了: 引用 刚才试了一下,好像不行,我是这样做的, jdbc:mysql:replication://192.168.1.1,192.168.1.1/xx 这个地址用Driver作为驱动 jdbc:mysql://192.168.1.2/xxx 这个地址也用Driver类作为驱动。 谢谢readonly大哥和downpour,我对你们非常的拜服啊 public Connection connect(String url, Properties info)
throws SQLException
{
Properties props;
if(url != null)
{
if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://"))
{
return connectLoadBalanced(url, info);
}
if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://"))
{
return connectReplicationConnection(url, info);
}
}
props = null;
if((props = parseURL(url, info)) == null)
{
return null;
}
com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);
return newConn;
SQLException sqlEx;
sqlEx;
throw sqlEx;
Exception ex;
ex;
throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + ex.toString() + Messages.getString("NonRegisteringDriver.18"), "08001");
}
从上面这段代码中可以看出,如果协议使用的是"jdbc:mysql:replication://",还是回返回ReplicationConnection的,所以不需要再使用ReplicationDriver |
|
| 返回顶楼 | |
|
时间:2008-07-02
在实际环境中,使用jdbc:mysql:replication://有一些问题,在小数据量的情况时没有问题,但是在大的批量任务的时候发现,master中的数据并不能复制到slave机器上,数据正常的插入了master,但是却没有复制slave
还有一个情况,使用jdbc:mysql:replication://的时候,slave机器负载非常高,都到90%了,不使用replication的时候只写master,负载只有30%。 看来这个jdbc:mysql:replication://还有些问题,需要详细的测试才行,不能贸然的用到生产环境中去 这可能意味者还是要修改代码才行。估计还是得用主贴中重写ReplicationDriver的方式才行。 或者说我的用法还是不对,我暂时也想不起来哪里会出问题,一个小小的connection会让一个很牛叉的db服务器负载达到90-100%。 而且我在mysql的文档上怎么也查不到jdbc:mysql:replication://这个协议,不知所措啊 |
|
| 返回顶楼 | |
|
时间:2008-07-07
master/slave的复制机制和jdbc driver无关,是靠服务器之间通讯来处理的,你应该从master的log入手,看看是否有任何replication error
偶之前使用master/slave机制很顺利,唯一遇到的bug就是它的round robin策略有点异常,不过这个bug很快就被修正了,而且国内外使用mysql master/slave的成功案例也非常多,jdbc:mysql:replication协议和ReplicationDriver其实用的是相同的代码,你说的cpu 90%,偶怀疑和复制有关系,建议你先去检查master/slave的日志和配置吧。 |
|
| 返回顶楼 | |







