工作中我们可能会遇到的一个问题,可能会出现多租户场景,这种情况下,我们不得不对我们的系统分库,对于每一个租户来说都是一个数据库,这个我们可能考虑到多数据源去解决,也是一个思路,这几天调研了mycat
做分库,下面慢慢分享这一个过程。
我们首先要解决这个问题,今天首先要出场的是ThreadLocal
,对于这个类我的解释是:
首先来一个接口BatmanTenant
,这个接口主要封装两个方法。
public interface BatmanTenant {
void setBatmanTenantId(String var1);
String getBatmanTenantId();
}
接着创建一个实现类TenantStore
去实现BatmanTenant
,一个静态变量CONTEXT
存取上下文的租户信息。
public class TenantStore implements BatmanTenant {
private static final ThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();
private static boolean isApplicationTenant = false;
private static String applicationTenantId;
private static final String TENANT_DEFAULT_ID = "t0";
public static void setTenantId(String tenantId) {
CONTEXT.set(tenantId);
}
public static String getTenantId() {
if (isApplicationTenant) {
return applicationTenantId;
}
String tenantId = CONTEXT.get();
if (tenantId == null || "".equals(tenantId)) {
tenantId = TENANT_DEFAULT_ID;
}
return tenantId;
}
public static void clear() {
CONTEXT.remove();
}
public static boolean isApplicationTenant() {
return isApplicationTenant;
}
public static void setApplicationTenant(boolean applicationTenant) {
isApplicationTenant = applicationTenant;
}
public static String getApplicationTenantId() {
return applicationTenantId;
}
public static void setApplicationTenantId(String applicationTenantId) {
TenantStore.applicationTenantId = applicationTenantId;
}
@Override
public void setBatmanTenantId(String s) {
setTenantId(s);
}
@Override
public String getBatmanTenantId() {
return getTenantId();
}
}
mycat是一个数据库中间件,也可以理解为是数据库代理。在架构体系中是位于数据库和应用层之间的一个组件,并且对于应用层是透明的,即数据库 感受不到mycat的存在,认为是直接连接的mysql数据库。
conf
目录下的server.xml文件
,修改mycat的用户名及密码。默认端口号是8066。<user name="root">
<property name="password">batmanproperty>
<property name="schemas">t1,t2property>
user>
conf
目录下的schema.xml
,将下面配置拷贝过去即可。
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="t1" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn_t1" />
<schema name="t2" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn_t2" />
<dataNode name="dn_t1" dataHost="dh" database="t1"/>
<dataNode name="dn_t2" dataHost="dh" database="t2"/>
<dataHost name="dh" maxCon="1000" minCon="10" balance="0" writeType="0"
dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<heartbeat>select user()heartbeat>
<writeHost host="tenant_db" url="localhost:3306" user="root" password="root">
<readHost host="tenant_db" url="localhost:3306" user="root" password="root"/>
writeHost>
dataHost>
mycat:schema>
启动:
./mycat start
查看启动状态:
./mycat status
停止:
./mycat stop
重启(改变上面的 xml 配置不用重启,管理端可以重新载入):
./mycat restart
查看 logs/ 下的 wrapper.log 和 mycat.log 可以查看运行时问题和异常。
mycat 启动日志:
cat ./logs/wrapper.log
mycat 应用日志:
cat ./logs/mycat.log
mybatis
的拦截器创建TenantInterceptor
这个文件,从StatementHandler
获取到BoundSql
对象,这样就获取到要执行的sql
,把mycat的配置和租户信息数据库配置好,利用反射写回BoundSql
的sql
属性。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class TenantInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantInterceptor.class);
private static final String SCHEMA_START = "/*mycat:schema=";
private static final String SCHEMA_END = "*/";
@Override
public Object intercept(Invocation invocation) throws Throwable {
String tenant = TenantStore.getTenantId();
if (tenant == null || "".equals(tenant)) {
return invocation.proceed();
}
StatementHandler statementHandler = realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
String sql = boundSql.getSql();
//LOGGER.debug("TenantInterceptor before sql:" + sql);
//add sql mycat hits for sql route
//sql = "/*!mycat:schema=" + tenant + "*/" + sql;
if (!sql.startsWith(SCHEMA_START)) {
StringBuilder stringBuilder = new StringBuilder(sql.length() + 30);
stringBuilder.append(SCHEMA_START);
stringBuilder.append(tenant);
stringBuilder.append(SCHEMA_END);
stringBuilder.append(sql);
sql = stringBuilder.toString();
}
LOGGER.debug("TenantInterceptor after sql:" + sql);
ReflectHelper.setFieldValue(boundSql, "sql", sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
/**
*
* 获得真正的处理对象,可能多层代理.
*
*/
@SuppressWarnings("unchecked")
private static <T> T realTarget(Object target) {
if (Proxy.isProxyClass(target.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(target);
return realTarget(metaObject.getValue("h.target"));
}
return (T) target;
}
}
配置拦截器,创建MultiTenantMyBatisConfiguration
,给每个SqlSessionFactory
对象添加拦截。
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, Interceptor.class})
public class MultiTenantMyBatisConfiguration {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addPageInterceptor() {
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(new TenantInterceptor());
}
}
}
创建两个数据库分别为t1,t2。两个数据库有都有demo这张表。测试下面接口,会出现不同的结果。
github地址:https://github.com/fafeidou/fast-cloud-nacos/tree/master/fast-common-examples/fast-common-tenant-example