最近上线的项目中数据库数据已经临近饱和,最大的一张表数据已经接近 3000W,百万数据的表也有几张,项目要求读数据(select)时间不能超过 0.05 秒,但实际情况已经不符合要求,explain 建立索引,使用 redis,ehcache 缓存技术也已经满足不了要求,所以开始使用读写分离技术,可能以后数据量上亿或者更多的时候,需要再去考虑分布式数据库的部署,但目前来看,读写分离 + 缓存 + 索引 + 表分区 + sql 优化 + 负载均衡是可以满足亿级数据量的查询工作的,现在就一起来看一下亲测可用的使用 spring 实现读写分离的步骤:
我们一般应用对数据库而言都是 "读多写少",也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案,
其中一个是主库,负责写入数据,我们称之为:写库;
其它都是从库,负责读取数据,我们称之为:读库;
那么,对我们的要求是:
1、读库和写库的数据一致;(这个是很重要的一个问题,处理业务逻辑要放在 service 层去处理,不要在 dao 或者 mapper 层面去处理)
2、写数据必须写到写库;
3、读数据必须到读库;
解决读写分离的方案有两种:应用层解决和中间件解决。
优点:
1、多数据源切换方便,由程序自动完成;
2、不需要引入中间件;
3、理论上支持任何数据库;
缺点:
1、由程序员完成,运维参与不到;
2、不能做到动态增加数据源;
优缺点:
优点:
1、源程序不需要做任何改动就可以实现读写分离;
2、动态添加数据源不需要重启程序;
缺点:
1、程序依赖于中间件,会导致切换数据库变得困难;
2、由中间件做了中转代理,性能有所下降;
相关中间件产品使用:
MySQL-proxy:http://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07
Amoeba for mysql:http://www.iteye.com/topic/188598 和 http://www.iteye.com/topic/1113437
在进入 Service 之前,使用 AOP 来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以 query、find、get 等开头的就走读库,其他的走写库。
- import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
- /**
- * 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可
- *
- * 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
- *
- * @author zhijun
- *
- */
- public class DynamicDataSource extends AbstractRoutingDataSource {
- @Override protected Object determineCurrentLookupKey() {
- // 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
- return DynamicDataSourceHolder.getDataSourceKey();
- }
- }
- class = "java" >
- /**
- *
- * 使用ThreadLocal技术来记录当前线程中的数据源的key
- *
- * @author zhijun
- *
- */
- public class DynamicDataSourceHolder {
- //写库对应的数据源key
- private static final String MASTER = "master";
- //读库对应的数据源key
- private static final String SLAVE = "slave";
- //使用ThreadLocal记录当前线程的数据源key
- private static final ThreadLocal holder = new ThreadLocal();
- /**
- * 设置数据源key
- * @param key
- */
- public static void putDataSourceKey(String key) {
- holder.set(key);
- }
- /**
- * 获取数据源key
- * @return
- */
- public static String getDataSourceKey() {
- return holder.get();
- }
- /**
- * 标记写库
- */
- public static void markMaster() {
- putDataSourceKey(MASTER);
- }
- /**
- * 标记读库
- */
- public static void markSlave() {
- putDataSourceKey(SLAVE);
- }
- }
- import org.apache.commons.lang3.StringUtils;
- import org.aspectj.lang.JoinPoint;
- /**
- * 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
- *
- * @author zhijun
- *
- */
- public class DataSourceAspect {
- /**
- * 在进入Service方法之前执行
- *
- * @param point 切面对象
- */
- public void before(JoinPoint point) {
- // 获取到当前执行的方法名
- String methodName = point.getSignature().getName();
- if (isSlave(methodName)) {
- // 标记为读库
- DynamicDataSourceHolder.markSlave();
- } else {
- // 标记为写库
- DynamicDataSourceHolder.markMaster();
- }
- }
- /**
- * 判断是否为读库
- *
- * @param methodName
- * @return
- */
- private Boolean isSlave(String methodName) {
- // 方法名以query、find、get开头的方法名走从库
- return StringUtils.startsWithAny(methodName, "query", "find", "get");
- }
- }
- jdbc.master.driver = com.mysql.jdbc.Driver jdbc.master.url = jdbc: mysql: //127.0.0.1:3306/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
- jdbc.master.username = root jdbc.master.password = 123456
- jdbc.slave01.driver = com.mysql.jdbc.Driver jdbc.slave01.url = jdbc: mysql: //127.0.0.1:3307/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
- jdbc.slave01.username = root jdbc.slave01.password = 123456
- class = "com.jolbox.bonecp.BoneCPDataSource"destroy - method = "close" >
- class = "com.jolbox.bonecp.BoneCPDataSource"destroy - method = "close" >
- class = "cn.itcast.usermanage.spring.DynamicDataSource" >
- class = "org.springframework.jdbc.datasource.DataSourceTransactionManager" >
- <!-- 定义事务策略 -->
- <tx:advice id="txAdvice" transaction-manager="transactionManager">
- <tx:attributes>
- <!--定义查询方法都是只读的 -->
- <tx:method name="query*" read-only="true" />
- <tx:method name="find*" read-only="true" />
- <tx:method name="get*" read-only="true" />
- <!-- 主库执行操作,事务传播行为定义为默认行为 -->
- <tx:method name="save*" propagation="REQUIRED" />
- <tx:method name="update*" propagation="REQUIRED" />
- <tx:method name="delete*" propagation="REQUIRED" />
- <!--其他方法使用默认事务策略 -->
- <tx:method name="*" />
- </tx:attributes>
- </tx:advice>
- class = "cn.itcast.usermanage.spring.DataSourceAspect"id = "dataSourceAspect" / >
之前的实现我们是将通过方法名匹配,而不是使用事务策略中的定义,我们使用事务管理策略中的规则匹配。
- class = "cn.itcast.usermanage.spring.DataSourceAspect"id = "dataSourceAspect" >
- import java.lang.reflect.Field;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import org.apache.commons.lang3.StringUtils;
- import org.aspectj.lang.JoinPoint;
- import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
- import org.springframework.transaction.interceptor.TransactionAttribute;
- import org.springframework.transaction.interceptor.TransactionAttributeSource;
- import org.springframework.transaction.interceptor.TransactionInterceptor;
- import org.springframework.util.PatternMatchUtils;
- import org.springframework.util.ReflectionUtils;
- /**
- * 定义数据源的AOP切面,该类控制了使用Master还是Slave。
- *
- * 如果事务管理中配置了事务策略,则采用配置的事务策略中的标记了ReadOnly的方法是用Slave,其它使用Master。
- *
- * 如果没有配置事务管理的策略,则采用方法名匹配的原则,以query、find、get开头方法用Slave,其它用Master。
- *
- * @author zhijun
- *
- */
- public class DataSourceAspect {
- private List slaveMethodPattern = new ArrayList();
- private static final String[] defaultSlaveMethodStart = new String[] {
- "query",
- "find",
- "get"
- };
- private String[] slaveMethodStart;
- /**
- * 读取事务管理中的策略
- *
- * @param txAdvice
- * @throws Exception
- */
- @SuppressWarnings("unchecked") public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception {
- if (txAdvice == null) {
- // 没有配置事务管理策略
- return;
- }
- //从txAdvice获取到策略配置信息
- TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource();
- if (! (transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) {
- return;
- }
- //使用反射技术获取到NameMatchTransactionAttributeSource对象中的nameMap属性值
- NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource;
- Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
- nameMapField.setAccessible(true); //设置该字段可访问
- //获取nameMap的值
- Map map = (Map) nameMapField.get(matchTransactionAttributeSource);
- //遍历nameMap
- for (Map.Entry entry: map.entrySet()) {
- if (!entry.getValue().isReadOnly()) { //判断之后定义了ReadOnly的策略才加入到slaveMethodPattern
- continue;
- }
- slaveMethodPattern.add(entry.getKey());
- }
- }
- /**
- * 在进入Service方法之前执行
- *
- * @param point 切面对象
- */
- public void before(JoinPoint point) {
- // 获取到当前执行的方法名
- String methodName = point.getSignature().getName();
- boolean isSlave = false;
- if (slaveMethodPattern.isEmpty()) {
- // 当前Spring容器中没有配置事务策略,采用方法名匹配方式
- isSlave = isSlave(methodName);
- } else {
- // 使用策略规则匹配
- for (String mappedName: slaveMethodPattern) {
- if (isMatch(methodName, mappedName)) {
- isSlave = true;
- break;
- }
- }
- }
- if (isSlave) {
- // 标记为读库
- DynamicDataSourceHolder.markSlave();
- } else {
- // 标记为写库
- DynamicDataSourceHolder.markMaster();
- }
- }
- /**
- * 判断是否为读库
- *
- * @param methodName
- * @return
- */
- private Boolean isSlave(String methodName) {
- // 方法名以query、find、get开头的方法名走从库
- return StringUtils.startsWithAny(methodName, getSlaveMethodStart());
- }
- /**
- * 通配符匹配
- *
- * Return if the given method name matches the mapped name.
- * <p>
- * The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, as well as direct
- * equality. Can be overridden in subclasses.
- *
- * @param methodName the method name of the class
- * @param mappedName the name in the descriptor
- * @return if the names match
- * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
- */
- protected boolean isMatch(String methodName, String mappedName) {
- return PatternMatchUtils.simpleMatch(mappedName, methodName);
- }
- /**
- * 用户指定slave的方法名前缀
- * @param slaveMethodStart
- */
- public void setSlaveMethodStart(String[] slaveMethodStart) {
- this.slaveMethodStart = slaveMethodStart;
- }
- public String[] getSlaveMethodStart() {
- if (this.slaveMethodStart == null) {
- // 没有指定,使用默认
- return defaultSlaveMethodStart;
- }
- return slaveMethodStart;
- }
- }
很多实际使用场景下都是采用 "一主多从" 的架构的,所以我们现在对这种架构做支持,目前只需要修改 DynamicDataSource 即可。
- import java.lang.reflect.Field;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.atomic.AtomicInteger;
- import javax.sql.DataSource;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
- import org.springframework.util.ReflectionUtils;
- /**
- * 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可
- *
- * 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
- *
- * @author zhijun
- *
- */
- public class DynamicDataSource extends AbstractRoutingDataSource {
- private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);
- private Integer slaveCount;
- // 轮询计数,初始为-1,AtomicInteger是线程安全的
- private AtomicInteger counter = new AtomicInteger( - 1);
- // 记录读库的key
- private List slaveDataSources = new ArrayList(0);
- @Override protected Object determineCurrentLookupKey() {
- // 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
- if (DynamicDataSourceHolder.isMaster()) {
- Object key = DynamicDataSourceHolder.getDataSourceKey();
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("当前DataSource的key为: " + key);
- }
- return key;
- }
- Object key = getSlaveKey();
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("当前DataSource的key为: " + key);
- }
- return key;
- }
- @SuppressWarnings("unchecked")@Override public void afterPropertiesSet() {
- super.afterPropertiesSet();
- // 由于父类的resolvedDataSources属性是私有的子类获取不到,需要使用反射获取
- Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources");
- field.setAccessible(true); // 设置可访问
- try {
- Map resolvedDataSources = (Map) field.get(this);
- // 读库的数据量等于数据源总数减去写库的数量
- this.slaveCount = resolvedDataSources.size() - 1;
- for (Map.Entry entry: resolvedDataSources.entrySet()) {
- if (DynamicDataSourceHolder.MASTER.equals(entry.getKey())) {
- continue;
- }
- slaveDataSources.add(entry.getKey());
- }
- } catch(Exception e) {
- LOGGER.error("afterPropertiesSet error! ", e);
- }
- }
- /**
- * 轮询算法实现
- *
- * @return
- */
- public Object getSlaveKey() {
- // 得到的下标为:0、1、2、3……
- Integer index = counter.incrementAndGet() % slaveCount;
- if (counter.get() > 9999) { // 以免超出Integer范围
- counter.set( - 1); // 还原
- }
- return slaveDataSources.get(index);
- }
- }
mysql 主 (称 master) 从(称 slave)复制的原理:
1、master 将数据改变记录到二进制日志 (binarylog) 中, 也即是配置文件 log-bin 指定的文件(这些记录叫做二进制日志事件,binary log events)
2、slave 将 master 的 binary logevents 拷贝到它的中继日志 (relay log)
3、slave 重做中继日志中的事件, 将改变反映它自己的数据 (数据重演)
1、主 DB server 和从 DB server 数据库的版本一致
2、主 DB server 和从 DB server 数据库数据一致 [这里就会可以把主的备份在从上还原,也可以直接将主的数据目录拷贝到从的相应数据目录]
3、主 DB server 开启二进制日志, 主 DB server 和从 DB server 的 server_id 都必须唯一
可能有些朋友主从数据库的 ip 地址、用户名和账号配置不是很清楚,下面是我测试的主从配置,ip 都是 127.0.0.1,我在讲完自己的例子后,还会写
一个主从 ip 是不相同的配置的例子,大家可以通过这个例子去更加直观的了解配置方法。
在 my.ini [mysqld] 下面修改(从库也是如此):
#开启主从复制,主库的配置
log-bin= mysql3306-bin
#指定主库 serverid
server-id=101
#指定同步的数据库,如果不指定则同步全部数据库
binlog-do-db=mybatis_1128
执行 SQL 语句查询状态:
SHOW MASTER STATUS
需要记录下 Position 值,需要在从库中设置同步起始值。
另外我再说一点,如果您在 mysql 执行 SHOW MASTER STATUS 发现配置在 my.ini 中的内容没有起到效果,可能原因是并没有选择对 my.ini 文件,也可能是您没有重启服务,很大概率是后者造成的原因,
要想使配置生效,必须关掉 MySQL 服务,再重新启动。
关闭服务的方法:
win 键打开,输入 services.msc 调出服务:
再启动 SQLyog,发现配置已经生效了。
#授权用户 slave01 使用 123456 密码登录 mysql
grant replication slave on *.* to 'slave01'@'127.0.0.1'identified by '123456';
flush privileges;
在 my.ini 修改:
#指定 serverid,只要不重复即可,从库也只有这一个配置,其他都在 SQL 语句中操作
server-id=102
以下执行 SQL:
CHANGE MASTER TO
master_host='127.0.0.1',
master_user='slave01',
master_password='123456',
master_port=3306,
master_log_file='mysql3306-bin.000006',
master_log_pos=1120;
#启动 slave 同步
START SLAVE;
#查看同步状态
SHOW SLAVE STATUS;
下面是 ip 不同的两台电脑的主从配置方法:
主数据库所在的操作系统:win7
主数据库的版本:5.0
主数据库的 ip 地址:192.168.1.111
从数据库所在的操作系统:linux
从数据的版本:5.0
从数据库的 ip 地址:192.168.1.112
介绍完了环境,就聊聊配置步骤:
1、确保主数据库与从数据库一模一样。
例如:主数据库里的 a 的数据库里有 b,c,d 表,那从数据库里的就应该有一个模子刻出来的 a 的数据库和 b,c,d 表
2、在主数据库上创建同步账号。
GRANT REPLICATION SLAVE,FILE ON *.* TO 'mstest'@'192.168.1.112' IDENTIFIED BY '123456';
192.168.1.112:是运行使用该用户的 ip 地址
mstest:是新创建的用户名
123456:是新创建的用户名的密码
以上命令的详细解释,最好百度一下,写太多反到更加更不清思路。
3、配置主数据库的 my.ini(因为是在 window 下,所以是 my.ini 不是 my.cnf)。
[mysqld]
server-id=1
log-bin=log
binlog-do-db=mstest // 要同步的 mstest 数据库, 要同步多个数据库,就多加几个 replicate-db-db = 数据库名
binlog-ignore-db=mysql // 要忽略的数据库
4、配置从数据库的 my.cnf。
[mysqld]
server-id=2
master-host=192.168.1.111
master-user=mstest // 第一步创建账号的用户名
master-password=123456 // 第一步创建账号的密码
master-port=3306
master-connect-retry=60
replicate-do-db=mstest // 要同步的 mstest 数据库, 要同步多个数据库,就多加几个 replicate-db-db = 数据库名
replicate-ignore-db=mysql // 要忽略的数据库
5、验证是否成功
进入 mysql,后输入命令: show slave status\G。将显示下图。如果 slave_io_running 和 slave_sql_running 都为 yes,那么表明可以成功同步了
6、测试同步数据。
进入主数据库输入命令: insert into one(name) values('beijing');
然后进入从数据库输入命令:select * from one;
如果此时从数据库有获取到数据,说明同步成功了,主从也就实现了
该不同 ip 主从配置转自:http://www.cnblogs.com/sustudy/p/4174189.html
来源: http://www.cnblogs.com/fengwenzhee/p/7193218.html