目录
1 、为什么需要读写分离
1.1 读写分离的背景
数据量很大时候,我们的数据库面临着很大的压力,这时候我们需要从架构方面来解决这一问题,在一个网站中读的操作很多,写的操作很少,这时候我们需要配置读写分离,把读操作和写操作分离出来,最大程度的利用好数据库服务器。读写分离的实现就是在执行SQL的时候,根据判断读操作还是写操作映射到不同的数据库上。
1.2 读写分离的原理
读写分离的实现原理如下图所示:
-
主节点必须启用二进制日志,记录任何修改了数据库数据的事件。
-
从节点开启一个线程(I/O Thread)把自己扮演成 MySQL的客户端,通过 MySQL协议,请求主节点的二进制日志文件中的事件
-
主节点启动一个线程(DumpThread),检查自己二进制日志中的事件,跟对方请求的位置对比,如果不带请求位置参数,则主节点就会从第一个日志文件中的第一个事件一个一个发送给从节点。
-
从节点接收到主节点发送过来的数据把它放置到中继日志(Relay log)文件中。并记录该次请求到主节点的具体哪一个二进制日志文件内部的哪一个位置(主节点中的二进制文件会有多个)。
-
从节点启动另外一个线程(SQL Thread ),把 Relay log 中的事件读取出来,并在本地再执行一次。
1.3 复制中线程的作用
从节点:
-
I/O Thread: 从 Master 节点请求二进制日志事件,并保存于中继日志中。
-
SQL Thread: 从Relay log 中读取日志事件并在本地完成重放。
主节点:
-
Dump Thread:为每个 Slave 的 I/O Thread 启动一个 dump 线程,用于向从节点发送二进制事件。
思考:从节点需要建立二进制日志文件吗? 看情况,如果从节点需要作为其他节点的主节点时,是需要开启二进制日志文件的。这种情况叫做级联复制。如果只是作为从节点,则不需要创建二进制文件
1.4 MySQL复制特点
异步复制:主节点中一个用户请求一个写操作时,主节点不需要把写的数据在本地操作完成同时发送给从服务器并等待从服务器反馈写入完成,再响应用户。主节点只需要把写入操作在本地完成,就响应用户。但是,从节点中的数据有可能会落后主节点,为什么?因为网络通讯需要时间!
2、主从复制实现
2.1 主从节点配置过程
主节点:
1)、启用二进制日志。
2)、为当前节点设置一个全局唯一的server_id。
3)、创建有复制权限的用户账号 REPLIACTION SLAVE ,REPLIATION CLIENT。
从节点:
1)、启动中继日志。
2)、为当前节点设置一个全局唯一的server_id。
3)、使用有复制权限的用户账号连接至主节点,并启动复制线程。
准备两台服务器安装Mysql
192.168.223.128(主)
192.168.223.129(从)
rpm -Uvh http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm #下载
yum -y install mysql-community-server #rpm安装
#加入开机启动
systemctl enable mysqld
#启动mysql服务进程
systemctl start mysqld
#初始化,执行命令,重置密码
mysql_secure_installation
#会依次出现以下问题。
Set root password? [Y/n]
是否设置root用户的密码 (y后【设置登录密码】)
Remove anonymous users? [Y/n]
是否删除匿名用户 (y)
Disallow root login remotely? [Y/n]
是否禁止root远程登录 (n)
Remove test database and access to it? [Y/n]
是否删除test数据库(y)
Reload privilege tables now? [Y/n]
是否重新加载授权信息 (y)
# 先进入mysql
mysql -u root -p
# 授权(root用户)远程连接权限(不建议)
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '远程登录密码' WITH GRANT OPTION;
FLUSH PRIVILEGES;
# 使用单独的远程登录用户(推荐)
GRANT ALL PRIVILEGES ON *.* TO '新用户名'@'%' IDENTIFIED BY '远程登录密码' WITH GRANT OPTION;
FLUSH PRIVILEGES;
配置mysql主从:
#------------------------------------------#主库设置-------------------------------------------#
#主库修改配置 vim /etc/my.cnf,具体位置可能不一样,通过whereis my.cnf查看,有可能在usr/my.cnf下
#主库配置添加如下
server-id = 1
log-bin = master-bin
log-bin-index = master-bin.index
#3:主库 从库重启
service mysql restart
#4:主库设置
mysql -uroot -proot #进入主库
#查看主库状态
show MASTER status;
#创建主从连接账户repl
CREATE USER repl;
#提供给从库账号repl密码123456 跟主库连接,有slave的权限
GRANT replication slave ON *.* TO 'repl'@'192.168.223.129' identified by '123456';
#设置用户权限 主库可以增删改查(当然你高兴直接用root也行)
#创建主库操作账户work
CREATE USER work;
GRANT SELECT,UPDATE,DELETE, INSERT ON *.* TO 'work'@'%' identified by '123456' WITH GRANT OPTION;
#刷新生效
FLUSH PRIVILEGES;
#查询repl权限
show grants for 'repl';
#------------------------------------------#5:从库设置-------------------------------------------#
#从库修改配置 vim /etc/my.cnf,配置添加如下
server-id = 2
relay-log-index = relay-log.index
relay-log = relay-log
#进入从库
mysql -uroot -proot
#关闭备份
STOP SLAVE;
#创建主库连接,master_log_file和master_log_pos的值需要show MASTER status查询
CHANGE MASTER TO
master_host='192.168.223.128',
master_port=3306,
master_user='repl',
master_password='123456',
master_log_file='master-bin.000001',
master_log_pos=120;#小于当前master-bin日志binlog值,通过主节点show MASTER status;命令查看
#跳过当前时间来自于master的之后N个事件,每跳过一个event,则N--,N为1,只跳过当前错误事件
set GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
#开启备份
START SLAVE;
show SLAVE status;#查看从节点状态
PS:如果发现错误信息如下:
Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work.
如果是直接复制的虚拟机镜像文件,可能他们的auto.cnf中的server-uuid是一样的:vim /var/lib/mysql/auto.cnf
问题解决:
停止从库的mysqld服务,删除他的auto.cnf文件,再启动数据库服务即可:
service mysqld stop
mv /var/lib/mysql/auto.cnf /var/lib/mysql/auto.cnf.bak
service mysqld start
此时再去查看从库auto.cnf,已自动生成新的server-uuid
#设置work用户权限 从库使用work 只能查询数据 主库可以增删改查(以防万一而已!)
create user 'work'@'%' identified by '123456'; #//从库创建work用户
grant select on *.* to 'work'@'%' WITH GRANT OPTION; #//该用户只有select 权限
flush privileges; #//刷新生效
show grants for 'work'; #查询work权限
补充命令:
drop user repl@'%'; #删除用户repl
测试:
修改主库数据,从库也会跟着变!
PS:如果报错如下,是因为slave配置没生效,需要重置:
登录MySQL从节点,执行如下命令:
mysql> stop slave;
Query OK, 0 rows affected (0.00 sec)
mysql> reset slave;
Query OK, 0 rows affected (0.01 sec)
mysql> start slave;
Query OK, 0 rows affected (0.00 sec)
2.2 SpringBoot读写分离代码实现
前边根据配置我们实现了主从分离,主库添加数据,从库可以查询。然后我们通过代码实现连接两个数据源,
在此我们思考,由于主从库的数据需要同步,从库的数据可能及时性差点,受到网络影响晚点才能同步过去,这时候从库查询不到,所以代码应该能够灵活实现比如:插入主库之后直接从主库查询
实现读写分离的核心是 AbstractRoutingDataSource,这个抽象类中定义抽象方法,通过Map存放多个数据源,然后通过key的值指定具体要是用的数据源。
代码实现(基于SpringBoot + MySQL + Mybatis+ AOP):
2.2.1 pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
2.2.2 数据源配置
# 数据源写
spring.datasource.druid.write.url=jdbc:mysql://192.168.223.128:3306/db01
spring.datasource.druid.write.username=root
spring.datasource.druid.write.password=root
spring.datasource.druid.write.driver-class-name=com.mysql.jdbc.Driver
# 数据源读
spring.datasource.druid.read.url=jdbc:mysql://192.168.223.129:3306/db01
spring.datasource.druid.read.username=work
spring.datasource.druid.read.password=123456
spring.datasource.druid.read.driver-class-name=com.mysql.jdbc.Driver
2.2.3 动态数据源
package com.ydt.springmysqlmasterslave.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
/**
* spring 在开始进行数据库操作时会通过determineCurrentLookupKey 这个方法来决定使用哪个数据库,
* 因此我们在这里调用上面 DbContextHolder 类的getDbType()方法获取当前操作类别,同时可进行读库的负载均衡
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
String lookupKey = DynamicDataSourceHolder.getDataSource();
System.out.println("使用了数据库:" + lookupKey);
return lookupKey;
}
}
2.2.4 动态数据源线程绑定
package com.ydt.springmysqlmasterslave.datasource;
/**
* 用来设置数据库类别,其中有一个 ThreadLocal 用来保存每个线程的是使用读库,还是写库
*/
public class DynamicDataSourceHolder {
// 使用ThreadLocal把数据源与当前线程绑定
private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
public static void setDataSource(String dataSourceName) {
dataSources.set(dataSourceName);
}
public static String getDataSource() {
return (String) dataSources.get();
}
public static void clearDataSource() {
dataSources.remove();
}
}
2.2.5 数据源配置类
package com.ydt.springmysqlmasterslave.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置
*
* @author Administrator
*/
@Configuration
public class DataSourceConfig {
public final static String WRITE_DATASOURCE_KEY = "WRITE";
public final static String READ_DATASOURCE_KEY = "READ";
@ConfigurationProperties(prefix = "spring.datasource.druid.read")
@Bean(name = READ_DATASOURCE_KEY)
public DataSource readDruidDataSource() {
return new DruidDataSource();
}
@ConfigurationProperties(prefix = "spring.datasource.druid.write")
@Bean(name = WRITE_DATASOURCE_KEY)
public DataSource writeDruidDataSource() {
return new DruidDataSource();
}
/**
* 注入AbstractRoutingDataSource
*
* @return
* @throws Exception
*/
@Bean("routingDataSource")
public AbstractRoutingDataSource
routingDataSource( @Qualifier(READ_DATASOURCE_KEY) DataSource readDruidDataSource,
@Qualifier(WRITE_DATASOURCE_KEY) DataSource writeDruidDataSource) throws Exception {
DynamicDataSource proxy = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
targetDataSources.put(WRITE_DATASOURCE_KEY, writeDruidDataSource);
targetDataSources.put(READ_DATASOURCE_KEY, readDruidDataSource);
proxy.setTargetDataSources(targetDataSources);// 配置数据源
proxy.setDefaultTargetDataSource(writeDruidDataSource);// 默认为主库用于写数据
return proxy;
}
}
2.2.6 AOP实现数据源切换
package com.ydt.springmysqlmasterslave.aspect;
import com.ydt.springmysqlmasterslave.annotation.ReadOnly;
import com.ydt.springmysqlmasterslave.datasource.DataSourceConfig;
import com.ydt.springmysqlmasterslave.datasource.DynamicDataSourceHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import static com.ydt.springmysqlmasterslave.datasource.DataSourceConfig.WRITE_DATASOURCE_KEY;
@Aspect
@Component
public class DynamicDataSourceAspect {
@Around("execution(* com.ydt.springmysqlmasterslave.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method targetMethod = methodSignature.getMethod();
if (method.isAnnotationPresent(ReadOnly.class)) {
System.out.println("---进入只读数据源-----");
DynamicDataSourceHolder.setDataSources(DataSourceConfig.READ_DATASOURCE_KEY);
}else{
System.out.println("---进入主数据源-----");
DynamicDataSourceHolder.setDataSources(DataSourceConfig.WRITE_DATASOURCE_KEY);
}
// 执行方法
Object result = pjp.proceed();
//避免对后续在本线程上执行的操作产生影响
DynamicDataSourceHolder.clearDataSource();
return result;
}
}
2.2.7 只读注解
package com.ydt.springmysqlmasterslave.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadOnly {
}
2.2.8 Mybatis配置
package com.ydt.springmysqlmasterslave.datasource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
@Resource(name = "routingDataSource")
private DataSource routingDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(routingDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:com/ydt/springmysqlmasterslave/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(routingDataSource);
}
}
2.2.9 其他业务代码
service:
package com.ydt.springmysqlmasterslave.service;
import com.ydt.springmysqlmasterslave.domain.GoodStore;
public interface GoodStoreService {
public void addGoodStore(String code , int store);
public GoodStore getByCode(String code);
}
package com.ydt.springmysqlmasterslave.service.impl;
import com.ydt.springmysqlmasterslave.annotation.ReadOnly;
import com.ydt.springmysqlmasterslave.domain.GoodStore;
import com.ydt.springmysqlmasterslave.mapper.GoodStoreMapper;
import com.ydt.springmysqlmasterslave.service.GoodStoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class GoodStoreServiceImpl implements GoodStoreService {
@Autowired
private GoodStoreMapper goodStoreMapper;
public void addGoodStore(String code , int store) {
GoodStore goodStore = new GoodStore();
goodStore.setCode(code);
goodStore.setStore(store);
System.out.println(goodStoreMapper.save(goodStore));
//插完之后立马查,其实查的是主库,因为我们AOP切面切入的是service方法
System.out.println("插完之后立马查:" + getByCode(code));
}
/**
* 比如通过@ReadOnly来让该方法走只读数据库
* @param code
* @return
*/
@ReadOnly
public GoodStore getByCode(String code) {
return goodStoreMapper.findByCode(code);
}
}
dao:
package com.ydt.springmysqlmasterslave.mapper;
import com.ydt.springmysqlmasterslave.domain.GoodStore;
public interface GoodStoreMapper {
boolean save(GoodStore goodStore);
GoodStore findByCode(String code);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.ydt.springmysqlmasterslave.mapper.GoodStoreMapper" >
<insert id="save" parameterType="com.ydt.springmysqlmasterslave.domain.GoodStore">
insert into tb_goods_store(code,store) values (#{code},#{store})
</insert>
<select id="findByCode" resultType="com.ydt.springmysqlmasterslave.domain.GoodStore">
select * from tb_goods_store where code = #{code}
</select>
</mapper>
实体类:
package com.ydt.springmysqlmasterslave.domain;
public class GoodStore {
private String code ;
private int store;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public int getStore() {
return store;
}
public void setStore(int store) {
this.store = store;
}
@Override
public String toString() {
return "GoodStore{" +
"code='" + code + '\'' +
", store=" + store +
'}';
}
}
Test:
package com.ydt.springmysqlmasterslave;
import com.ydt.springmysqlmasterslave.service.GoodStoreService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringMysqlMasterSlaveApplicationTests {
@Autowired
private GoodStoreService goodStoreService;
@Test
public void testAdd(){
goodStoreService.addGoodStore("001",100);
//两个数据库线程,会切换到只读库,可能查不到数据
System.out.println(goodStoreService.getByCode("001"));
}
@Test
public void testQuery(){
System.out.println(goodStoreService.getByCode("001"));
}
}
3、MyCat实现读写分离
在MySQL中间件出现之前,对于MySQL主从集群,如果要实现其读写分离,一般是在程序端实现,这样就带来一个问题,即数据库和程序的耦合度太高,如果我数据库的地址发生改变了,那么我程序端也要进行相应的修改,如果数据库不小心挂掉了,则同时也意味着程序的不可用,而这对很多应用来说,并不能接受。
引入MySQL中间件能很好的对程序端和数据库进行解耦,这样,程序端只需关注数据库中间件的地址,而无需知晓底层数据库是如何提供服务。
作为当前炙手可热的MySQL中间件,MyCAT实现MySQL主从集群的读写分离自是应有之义,其配置也相当简单。
3.1 MyCat主从复制读写分离中的应用
配置MyCat的读写分离配置:
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!-- 设置表的存储方式.schema name="TESTDB" 与 server.xml中的 TESTDB 设置一致 -->
<schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100">
<table name="tb_goods_store" dataNode="dn1" />
</schema>
<!-- 设置表的存储方式.schema name="TESTDB" 与 server.xml中的 TESTDB 设置一致 -->
<dataNode name="dn1" dataHost="localhost1" database="test" />
<!-- mycat 逻辑主机dataHost对应的物理主机.其中也设置对应的mysql登陆信息 -->
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="3"
writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!-- can have multi write hosts -->
<writeHost host="hostM1" url="jdbc:mysql://192.168.223.128:3306" user="root" password="root">
<readHost host="hostS1" url="jdbc:mysql://192.168.223.129:3306" user="root" password="root"/>
</writeHost>
</dataHost>
</mycat:schema>
<!--
balance:
0:不开启读写分离,所有读操作都发生在当前的writeHost上
1.所有读操作都随机发送到当前的writeHost对应的readHost和备用的writeHost
2.所有的读操作都随机发送到所有的writeHost,readHost上
3.所有的读操作都只发送到writeHost的readHost上
writeType
0: 所有写操作发送到配置的第一个writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后已切换后的为准,切换记录在配置文件中:dnindex.properties
1:所有写操作都随机的发送到配置的writeHost。
2:所有的writeHost节点不再执行写的操作,所有的writeHost都停止写操作,只供查询的操作(查询压力比较大的时候才会使用到)
switchType:注意,这个配置只是对<writeHost>来说,两个主从数据库也可以都配置为<writeHost>,一般来说这个地方会要将两个数据库配置为互为主从关系
-1:不自动切换,实在想不出有啥用?配置多个<writeHost>的意义何在
1:自动切换
2:基于MySQL主从同步的状态决定是否切换,心跳语句为 show slave status
-->
使用MyCAT的逻辑库就和使用MySQL本身的库一样,其他没有任何区别
spring:
datasource:
url: jdbc:mysql://192.168.223.128:8066/TESTDB
username: test
password: test
driver-class-name: com.mysql.jdbc.Driver
3.2 Mycat应用场景
1:单纯的读写分离,此时配置最为简单,支持读写分离,主从切换;
2:分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片; 3:多租户应用,每个应用一个库,但应用程序只连接Mycat,从而不改造程序本身,实现多租户化; 4:报表系统,借助于Mycat的分表能力,处理大规模报表的统计; 5:替代Hbase,分析大数据;作为海量数据实时查询的一种简单有效方案,比如100亿条频繁查询的记录需要在3秒内查询出来结果,除了基于主键的查询,还可能存在范围查询或其他属性查询,此时Mycat可能是最简单有效的选择; 6:不断强化Mycat开源社区的技术水平,吸引更多的IT技术专家,使得Mycat社区成为中国的Apache,并将Mycat推到Apache基金会,成为国内顶尖开源项目
3.3 Mycat不适合的应用场景
1:设计使用Mycat时有非分片字段查询,请慎重使用Mycat,可以考虑放弃!
2:设计使用Mycat时有分页排序,请慎重使用Mycat,可以考虑放弃!(分页查询只能查询到分表之后的单表数据) 3:设计使用Mycat时如果要进行表JOIN操作,要确保两个表的关联字段具有相同的数据分布,否则请慎重使用Mycat,可以考虑放弃! 4:设计使用Mycat时如果有分布式事务,得先看是否得保证事务得强一致性,否则请慎重使用Mycat,可以考虑放弃!