假如你日后的工作,需要快速实现MySQL的读写分离功能,你一定会想起这篇文章。如果你再次回到这里,证明你已经迫切需要一个简单快捷的解决方案了--那就是MySQL官方驱动
层实现的读写分离,偏小众,但很有效。
JDBC驱动
我们经常使用的MySQL驱动jar包,其实默认有非常棒的功能,那就是主从分离和HA。如果你只是需要一个主从分离、failover的功能,不要sharding。一个驱动就够了,不需要引入什么中间层。
这个东西就是Replication协议。Mysql JDBC Connector在5.1.X版本之后增加了这些功能,以支持“multi-host”集群拓扑的访问范式。这个功能是在驱动层实现的,而既然是驱动层,那就不可避免有一些驱动层的问题(详见《“分库分表" ?选型和流程要慎重,否则会失控》。
我们平常的jdbc连接是这样
jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8
而经过协议改造后的jdbc连接,长得要长一些、大一些!
jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:3307,127.0.0.1:3308/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=false&loadBalanceStrategy=random
当然,也可以有ipv6的写法,更加直观
jdbc:mysql://address=(type=master)(host=master1host),address=(type=master)(host=master2host),address=(type=slave)(host=slave1host)/db
8.0的文档看这里:
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-multi-host-connections.html
说明:
协议的第一个连接
,表示主库Master
后面的一堆连接
,表示从库Slave
,当然可以有多个
当你把Master
的连接
也放在后面的一堆里,那么它也拥有了“读库“的属性了
后面有一堆参数,来控制这所有连接
,到底要如何相处
这样,所谓的主从分离功能,只要配置好这个连接串,就枯木逢春了。
代码层
首先你要改驱动类,再也不是Driver了,而是这个
com.mysql.jdbc.ReplicationDriver
这种情况下
DataSource.getConnection()
获取的连接,实际上是ReplicationConnection,这个连接是虚拟的,和真实的数据库连接是个1对多的关系,所以记得给每一个MySQL都做上相应的机器授权。
那么如何来区别本次请求是读是写呢?靠的其实是Connection
中的readonly
属性,这个属性是通过请求header
中的“readonly”传递的。所以如果请求中有写入操作,这整个的事务就一定是readonly=false的。
对于Spring来说,就可以使用@Transactional
注解来控制这个属性了。一个事务不可能跨两个连接,所以是读是写,有最高层决定。
以下代码片段展示了你应该配置的一些元素,没错,就是注解。
public interface UserManager {
public UserDO get(int id);
public void insert(UserDO user);
public void update(int id);
}
@Component
public class UserManagerImpl implements UserManager{
@Autowired
private UserDao userDao;
...
@Override
@Transactional(readOnly = false,propagation = Propagation.REQUIRED)
public void insert(UserDO user) {
this.userDao.insert(user);
}
}
是不是很简单?等等,别高兴太早。
参数
不要觉得是官方驱动,就可以任性的用。这套jdbc驱动的参数还是非常丰富的,学习的代价也就高了些。在一些小流量下运行的很好,但在高并发环境下会频繁发生问题。这里只挑最重要的说下。
一个虚拟连接,对应着一个真正的主库连接和多个从库连接。对于主机的存活和重新上线,我们要考虑三种情况:
- Master都死掉了
- Slave都死掉了
- Master、Slave全都死掉了
无论哪种情况,MySQL驱动都表现出一些奇怪的行为,默认参数是不好使的。
你可能以为驱动也就是管理一下几个连接而已,但情况比这复杂的多。首先给张图看下这个复杂的关系。
1、readFromMasterWhenNoSlaves 当所有的salve死掉后,此参数用来控制主库是否参与读。如果从库的流量很大,配置此参数对主库有很大风险;但如果你关掉,请求则会快速失败。
2、loadBalanceStrategy 策略用来指定从库的轮询规则。有轮询,也有权重,也可以指定具体的策略实现。当你维护或者迁移某个实例时,先置空流量,这会非常有用。或许,你会给某DB一个预热的可能。
3、allowMasterDownConnections 如果主机当机,当连接池获取新的连接时,会失败。但如果打开此参数,则虚拟连接只会创建Slave连接组,整个连接会降级为只读,不论你设置了什么注解。
4、allowSlavesDownConnections 如果没有只读库了,是否允许创建新的连接。在这种情况下,此参数开启,读操作有很大可能会失败。
5、retriesAllDown 当所有的hosts都无法连接时重试的最大次数(依次循环重试),默认为120。重试次数达到阈值仍然无法获取有效链接,将会抛出SQLException。
6、autoReconnect 实例既然有下线、就有上线。上线以后要能够继续服务,此参数用来控制断线情况下自动重连而不抛出异常。这会破坏事务的完整性,但还是默认开启。
然而MySQL驱动提供了更加丰富的参数来控制这个过程,如果你的业务要求比较苛刻,这些参数可能要测个遍才会放心。
但大多数情况下,它运行的很好。
管理
仅有配置参数,此协议就算一个半成品而已。所幸,此驱动提供了JMX管理的方式,可以基于其做一些配置变更之类的功能。市面上并没有这种配置管理工具,可能还是因为它太小众了。
public abstract void addSlaveHost(String groupFilter, String host) throws SQLException;
public abstract void removeSlaveHost(String groupFilter, String host) throws SQLException;
public abstract void promoteSlaveToMaster(String groupFilter, String host) throws SQLException;
public abstract void removeMasterHost(String groupFilter, String host) throws SQLException;
public abstract String getMasterHostsList(String group);
public abstract String getSlaveHostsList(String group);
public abstract String getRegisteredConnectionGroups();
public abstract int getActiveMasterHostCount(String group);
public abstract int getActiveSlaveHostCount(String group);
public abstract int getSlavePromotionCount(String group);
public abstract long getTotalLogicalConnectionCount(String group);
public abstract long getActiveLogicalConnectionCount(String group);
我们顺便提一下阿里的德鲁伊数据库连接池。如图,某些功能,只支持默认的单连接,对multi-host支持还是有限。
结尾
MySQL 5.1.x官方驱动出了这么个东西以后,其实宣告了很多小公司自研的某些小中间件的死亡。翻来服务,改写JDBC,不过就是为了管理个连接集合。
本文对象为专注基础设施研发的同学。有人看到的,不过是一堆参数而已;而真正去深入使用的人,会感到背脊通彻的寒冷。
当它小众时,你对它不屑一顾。然而当你一旦采用了某种方案,你却希望全世界都是关于它的描写。人生从来就没那么幸运,很多坑,还需要自己来踩。