前言
随着数据量的增长以及业务的调整变更,我们需要选择合适的技术及存储引擎对数据进行归类,调整,达到高并发、秒响应、低延迟及可扩展对现有程序的改造升级
问题&现状
- 任务重,时间紧,人力不足,不能够重构业务
- 平滑切换,客户无感知
- 自建git服务器一套代码,要兼容不同分支发布
解决方案
1.对于问题一,我们要采用oop to aop 的变成思维的转变,即通过切面来完成,尽量不改动原来的业务逻辑,尤其是之前的老系统,多分组,一个改动,可能导致几十个bug 的,横空出世,太太可怕了吧
2. 第二个问题就是兼容,略带框架思维,走配置化和开关,不仅可以起到有效的解耦,而且还可以做到有效的补偿,有问题及时解决
3. 总体方案 切面 + 配置化
案例分享
直接更换数据库引擎 &根据标记做数据源切换
spring &Mybatis
案例一:基于包扫描的单数据源
1. xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="maxActive" value="${windBird.maxActive}"/>
<property name="initialSize" value="${windBird.initialSize}"/>
<property name="maxWait" value="${windBird.maxWait}"/>
<property name="minIdle" value="${windBird.minIdle}"/>
<property name="timeBetweenEvictionRunsMillis" value="${windBird.timeBetweenEvictionRunsMillis}"/>
<property name="minEvictableIdleTimeMillis" value="${windBird.minEvictableIdleTimeMillis}"/>
<property name="validationQuery" value="${windBird.validationQuery}"/>
<property name="testWhileIdle" value="${windBird.testWhileIdle}"/>
<property name="testOnBorrow" value="${windBird.testOnBorrow}"/>
<property name="testOnReturn" value="${windBird.testOnReturn}"/>
<property name="poolPreparedStatements" value="${windBird.poolPreparedStatements}"/>
<property name="maxOpenPreparedStatements" value="${windBird.maxOpenPreparedStatements}"/>
</bean>
<bean id="newDataSource" parent="parentDataSource">
<property name="url">
<value>${jdbc_new.url}</value>
</property>
<property name="username">
<value>${jdbc_new.username}</value>
</property>
<property name="password">
<value>${jdbc_new.password}</value>
</property>
</bean>
<bean id="oldDataSource" parent="parentDataSource">
<property name="url">
<value>${jdbc_old.url}</value>
</property>
<property name="username">
<value>${jdbc_old.username}</value>
</property>
<property name="password">
<value>${jdbc_old.password}</value>
</property>
</bean>
<!--构造一个动态数据源 -->
<bean id="enhanceDynamicDataSource" class="com.windBird.service.impl.dataSource.enhanceDynamicDataSource"></bean>
<!--添加一个拦截器 -->
<bean id="enhanceMapperInterceptor" class="com.windBird.service.impl.dataSource.EnhanceMapperInterceptor"></bean>
<bean id="sqlSessionFactoryTestDataSource" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="enhanceDynamicDataSource"/>
<property name="typeAliasesPackage" value="com.windBird.service.entity"/>
<property name="mapperLocations">
<list>
<value>classpath:/sqlmap/test/*Mapper.xml</value>
<value>classpath:/sqlmap/test/enhance/*Mapper.xml</value>
</list>
</property>
<property name="plugins">
<list>
<ref bean="testMapperInterceptor"/>
</list>
</property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryTestDataSource"/>
<property name="basePackage" value="com.windBird.service.impl.test.dao"/>
<property name="annotationClass" value="com.windBird.mybatis.MyBatisRepository"/>
</bean>
<bean id="txManagerTestHistory"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="enhanceDynamicDataSource"/>
<qualifier value="testHistory"/>
</bean>
<tx:annotation-driven transaction-manager="txManagerTestHistory"/>
</beans>
2. 构造的动态数据源
package com.wind.bird.service.impl.dataSource;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @author windbird
* @Date 2022/12/4 22:03
* @ClassName EnhanceDynamicDataSource
* @desc: 构造动态数据源
*/
public class EnhanceDynamicDataSource extends AbstractRoutingDataSource {
/**
* 仅仅历史dao层切mapper
*/
private static final ThreadLocal<Boolean> testReadMark = ThreadLocal.withInitial(() -> false);
public static void setTestReadMark(Boolean Value){
testReadMark.set(Value);
}
public static Boolean getTestReadMark(){
return testReadMark.get();
}
/**
* 即将使用的新数据源
*/
@Resource
private DruidDataSource newDataSource;
/**
* 改造之前的旧数据源
*/
@Resource
private DruidDataSource oldDataSource;
@Override
protected DataSource determineTargetDataSource() {
return testReadMark.get()?newDataSource:oldDataSource;
}
@Override
protected Object determineCurrentLookupKey() {
return null;
}
@Override
public void afterPropertiesSet() {
}
}
3. 拦截器
package com.wind.bird.dataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;
/**
* @author windbird
* @Date 2022/12/4 22:15
* @ClassName EnhanceMapperInterceptor
* @desc:
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class EnhanceMapperInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
if(isInterceptor()){
/**
* 获取操作类型 update || query
*/
String opType = invocation.getMethod().getName();
/**
* 采用切dao 层
*/
if("query".equals(opType)){
EnhanceDynamicDataSource.setTestReadMark(true);
Object obj = changeMapperToEnchange(invocation,true);
EnhanceDynamicDataSource.setTestReadMark(false);
return obj;
}
}
return invocation.proceed();
}
private Object changeMapperToEnchange(Invocation invocation, boolean isChange) throws InvocationTargetException, IllegalAccessException {
if(isChange){
Object[] args = invocation.getArgs();
// 准备执行的MappedStatement
MappedStatement currMs = (MappedStatement) args[0];
Configuration config = currMs.getConfiguration();
String currMsId = currMs.getId();
// 对应的enhance版本MappedStatement
MappedStatement stockReportMs = config.getMappedStatement("enhance." + currMsId);
// 读操作切换sql
Executor executor = (Executor) invocation.getTarget();
args[0] = stockReportMs;
if (args.length == 6) {
// 替换BoundSql
args[5] = stockReportMs.getBoundSql(args[1]);
// 生成新的CacheKey
args[4] = executor.createCacheKey((MappedStatement) args[0],
args[1], (RowBounds) args[2], (BoundSql) args[5]);
}
}
return invocation.proceed();
}
/**
* todo:
* 是否拦截 默认拦截 具体的根据业务自己实现
* @return
*/
private boolean isInterceptor() {
return true;
}
@Override
public Object plugin(Object target) {
if(target instanceof Executor){
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
}
mapper 改造
<?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="gy.windbird.impl.dao.OldTableDao">
<resultMap id="ResultMap" type="oldTable">
<result property="id" column="id"></result>
<result property="createDate" column="create_date"></result>
<result property="modifyDate" column="modify_date"></result>
<result property="version" column="version"></result>
<result property="amount" column="amount"></result>
<collection property="oldDetailList" column="{amount=amount,pid=id}"
select="gy.windbird.impl.dao.OldTableDao.getListByPidAndAmount"></collection>
</resultMap>
<select id="getEntity" resultMap="ResultMap">
select id,create_date,modify_date,version,amount
from old_table where id = #{id}
</select>
<insert id="create" parameterType="SupplierStock">
insert into old_table (id,create_date, version,amount)
values(#{id},now(), 0,ifnull(#{amount},0))
</insert>
<update id="updateAmount" parameterType="SupplierStock">
update old_table set version = version + 1,
amount=ifnull(amount,0)+ifnull(#{amount},0) where id = #{id}
</update>
</mapper>
<?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="enhance.gy.windbird.impl.dao.OldTableDao">
<resultMap id="ResultMap" type="oldTable">
<result property="id" column="id"></result>
<result property="createDate" column="create_date"></result>
<result property="modifyDate" column="modify_date"></result>
<result property="version" column="version"></result>
<result property="amount" column="amount"></result>
<collection property="oldDetailList" column="{amount=amount,pid=id}"
select="enhance.gy.windbird.impl.dao.OldTableDao.getListByPidAndAmount"></collection>
</resultMap>
<select id="getEntity" resultMap="ResultMap">
select id,create_date,modify_date,version,amount
from old_table where id = #{id}
</select>
<insert id="create" parameterType="SupplierStock">
insert into old_table (id,create_date, version,amount)
values(#{id},now(), 0,ifnull(#{amount},0))
</insert>
<update id="updateAmount" parameterType="SupplierStock">
update old_table set version = version + 1,
amount=ifnull(amount,0)+ifnull(#{amount},0) where id = #{id}
</update>
</mapper>
mapper 层 改造 包结构
备注
<collection property="oldDetailList" column="{amount=amount,pid=id}" select="enhance.gy.windbird.impl.dao.OldTableDao.getListByPidAndAmount"></collection>
1. 这个的功能是一个主表查其对应的明细表
2. 改造的话,也要变动入=如上
3. 属性介绍:
property value="oldDetailList" 主类实体字段名
column 对应的调用子类传参
只有一个: column="id" id 是数据库字段
多个传参语法:column="{Dao层@Param Value值=数据库字段,Dao层@Param Value值=数据库字段,...}"
example:column="{amount=amount,pid=id}"
select 语法 对应命名空间+ 方法Mapper id
案例二:基于动态数据源改造
1.第一个跑通了,第二个就方便了,只是二次封装了一个
2. 基于自己项目封装的动态数据源
3. bean 替换增强
4. 直接上代码,mapper层就不再赘述
# bean 定义替换增强
package com.wind.bird.dataSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class DynamicDataSourcePostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (registry.containsBeanDefinition("dynamicDataSource")) {
registry.removeBeanDefinition("dynamicDataSource");
}
if (registry.containsBeanDefinition("enhanceDynamicDataSource")) {
registry.registerBeanDefinition("dynamicDataSource", registry.getBeanDefinition("enhanceDynamicDataSource"));
registry.removeBeanDefinition("enhanceDynamicDataSource");
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
# 扩张动态数据源
package com.wind.bird.dataSource;
import com.alibaba.druid.pool.DruidDataSource;
/**
* @Author: windbird
*/
@Service
public class EnhanceDynamicDataSource extends DynamicDataSource {
private static final ThreadLocal<Boolean> changeMark = ThreadLocal.withInitial(() -> false);
public static void setChangeMark(Boolean value) {
changeMark.set(value);
}
public static Boolean getChangeMark() {
return changeMark.get();
}
/**
* @param userId
*/
@Override
public void selectDynamicDataSourceByUserId(Long userId) {
changeCrmMark.set(false);
/**
* 自己逻辑
*/
super.selectDsReportByTenantId(tenantId);
}
.
.
.
@Override
public void selectXXXXByUserId(Long userId) {
changeCrmMark.set(false);
super.selectXXXXByUserId(userId);
}
@Override
protected DataSource determineTargetDataSource() {
if (getChangeMark()) {
return enhanceDataSource;
}
return super.determineTargetDataSource();
}
}
# 事务
package com.wind.bird.dataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
/**
* @desc: 事务处不分割用统一数据源,用完还原原来的状态
*/
public class EnhanceDataSourceTransactionManager extends DataSourceTransactionManager {
private static final ThreadLocal<Boolean> changeMark = ThreadLocal.withInitial(()->false);
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
if (ExtendReportHologresDataSource.getChangeMark()) {
changeMark.set(true);
ExtendReportHologresDataSource.setChangeMark(false);
}else{
changeMark.set(false); //防止多线程导致的并发错误
}
super.doBegin(transaction, definition);
}
@Override
protected void doCleanupAfterCompletion(Object transaction) {
ExtendReportHologresDataSource.setChangeMark(changeMark.get());
super.doCleanupAfterCompletion(transaction);
}
}
# 通过切面获取自己想要的数据
package com.wind.bird.enhance;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* @author windbird
*/
@Component
@Aspect
public class GetNeedMarkAop implements Serializable {
private static final Logger logger = LoggerFactory.getLogger(GetTenantAop.class);
@Around(value="execution(* com.wind.bird.service.enhance..*.*(..))")
public Object assignTenantMark(ProceedingJoinPoint joinPoint) throws Throwable {
String needMark =null;
List<Object> validArgs = new ArrayList<>();
Object[] args = joinPoint.getArgs();
if(null!=args && args.length>0){
//根据实际业务看是否需要排除 接口类
for (Object arg: args){
if(arg instanceof MultipartFile || arg instanceof HttpServletRequest || arg instanceof HttpServletResponse){
continue;
}else{
validArgs.add(arg);
}
}
int size = validArgs.size();
if(size>0){
for (int i = 0; i <size ; i++) {
Object inputArg= validArgs.get(i);
//一个字段的话,必需约定顺序,或者构造对象
if(inputArg instanceof Long){
try {
needMark =String.valueOf(inputArg);
if(StringUtils.isNotBlank(needMark) || !"NULL".equals(needMark.toUpperCase())){
//todo:自己逻辑处理
break;
}
} catch (Exception e) {
logger.error("获取报错:{}",e);
}
}
if(inputArg instanceof List){
try {
Object unitList = ((List) inputArg).get(0);
Field[] declaredFields = unitList.getClass().getSuperclass().getDeclaredFields();
for(Field f :declaredFields){
f.setAccessible(true);
if("needMark".equals(f.getName()) || "need_mark".equals(f.getName())){
needMark = String.valueOf(f.get(unitList));
if(StringUtils.isNotBlank(needMark) || !"NULL".equals(needMark.toUpperCase())){
//todo:自己逻辑处理
break;
}
}
}
break;
} catch (Exception e) {
logger.error("获取集合里面某一对象里面的失败:{}",e);
return joinPoint.proceed();
}
}
try {
Class<?> aClass = validArgs.get(i).getClass();
Field[] fs = aClass.getDeclaredFields();
for(Field f :fs){
f.setAccessible(true);
if("needMark".equals(f.getName()) || "need_mark".equals(f.getName())){
needMark = String.valueOf(f.get(validArgs.get(i)));
}
}
} catch (Exception e) {
logger.error("获取对象里面的,失败:{}",e);
return joinPoint.proceed();
}
if(StringUtils.isNotBlank(needMark) || !"NULL".equals(needMark.toUpperCase())){
//todo:自己逻辑处理
}
}
}
}
return joinPoint.proceed();
}
}
# 拦截器 & mapper 同第一个
结束语
这个是自己的改造历程,希望可以帮助大家,一起成长