公司C#转java后数据一直用的sqlserver,出于性能,性价比,安全性考虑将迁移到mysql。因为不能停服导数据,也担心程序会有问题直接切换不能回滚问题。基本流程决定先双写读sqlserver,运行一段时间查看数据是否一致,确保了mysql的写没问题。然后将读取切换到mysql,运行几天没问题,改成只读写mysql。以上流程可通过开关来控制双写,单写,读取数据。
常规的操作就是在sqlserver和mysql之上写一个manager类,通过if...esls来判断。
@RefreshScope
@Service
public class UserBanManager {
//1.sqlserver,2.tidb
@Value("${ban.get.data.switch}")
public Integer getDataSwitch;
//1.sqlserver,2.tidb,3.both
@Value("${ban.write.data.switch}")
public Integer writeDataSwitch;
@Autowired
private UserBanMysqlMapper userBanMysqlMapper;
@Autowired
private UserBanMapper userBanMapper;
public List<UserBanInfo> getAllBanList(LocalDateTime now){
if(getDataSwitch==1) {
return userBanMapper.getAllBanList(now);
}else {
return userBanMysqlMapper.getAllBanList(now);
}
}
public Integer updateBanInfoByUid(Integer id, Integer status, Integer lineType,String lineName){
if(writeDataSwitch==1) {
return userBanMapper.updateBanInfoByUid(id,status,lineType,lineName);
}else if(writeDataSwitch==2) {
return userBanMysqlMapper.updateBanInfoByUid(id,status,lineType,lineName);
}else {
try {
userBanMapper.updateBanInfoByUid(id,status,lineType,lineName);
} catch (Exception e) {
}
return userBanMysqlMapper.updateBanInfoByUid(id,status,lineType,lineName);
}
}
虽然能解决问题,但是让人感觉不高端。网上搜了下没找到有人写出更NB的办法,不过偶然的机会看到了有人用了代理解决别的问题的操作,于是就尝试了下最后竟然成了。
1.将之前sqlserver的mapper复制一份到mysql的mapper路径下,修改sql语句成mysql语法。保证mysql接口类继承对应的sqlserver类,保证方法名,入参一致。
@Mapper
public interface MysqlAlbAlbumMapper extends AlbAlbumMapper {
@Select("SELECT PhotoCount FROM Album WHERE Id=#{albumId}")
Integer getPhotoCount(Integer albumId);
}
2.给每套sqlserver+mysql的Mapper组合编写Manager类,同样继承sqlserver接口类
public interface AlbAlbumManager extends AlbAlbumMapper {
Class<AlbAlbumMapper> sqlServerMapper = AlbAlbumMapper.class;
Class<MysqlAlbAlbumMapper> mysqlMapper = MysqlAlbAlbumMapper.class;
String switchKey = "ibt_alb_album";
}
3.编写开关配置类
@Data
@RefreshScope
@ConfigurationProperties(prefix = "switch.config")
public class SwitchConfigProps {
private Map<String, String> tableSwitch = new LinkedHashMap<>();
}
4.创建自定义注解,属性值basePackages是设置manager类所在的包路径。注解加在springboot启动类上。通过自定义注解的@Import引导加载我们需要的类
@Retention(RetentionPolicy.RUNTIME)
@Import(ManagerImportBeanDefinitions.class)
public @interface JdkManagerScan {
String[] basePackages() default {};
}
@JdkManagerScan(basePackages = {"com.bitauto.i.manager.ibitauto"})
@EnableConfigurationProperties(SwitchConfigProps.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public SpringUtils springUtils() {
return new SpringUtils();
}
}
5.此类将代理类交给spring管理
public class ManagerImportBeanDefinitions implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(JdkManagerScan.class.getName()));
String[] basePackages = Safes.of(annoAttrs.getStringArray("basePackages"))
.filter(StringUtils::hasText)
.toArray(String[]::new);
//扫包拿到指定路径下manager
ManagerClassPathScanner commonScanner = new ManagerClassPathScanner(registry);
commonScanner.resetFilters(false);
commonScanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
Set<BeanDefinitionHolder> beanDefinitionHolders = commonScanner.doScan(basePackages);
Safes.of(beanDefinitionHolders)
.forEach(beanDefinitionHolder -> {
GenericBeanDefinition definition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(JdkManagerFactoryBean.class);//把manager类的代理实现交给spring
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
});
}
}
6.此类用来加载自定义注解路径下的类
public class ManagerClassPathScanner extends ClassPathBeanDefinitionScanner {
public ManagerClassPathScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
return super.doScan(basePackages);
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
}
7.创建代理类
public class JdkManagerFactoryBean<T> implements FactoryBean<T> {
private Class managerInterFace;
public void setManagerInterFace(Class managerInterFace) {
this.managerInterFace = managerInterFace;
}
public JdkManagerFactoryBean(Class<T> mapperInterface) {
this.managerInterFace = mapperInterface;
}
@Override
public T getObject() throws Exception {
return (T) new JdkProxyManagerFactory().getManager(managerInterFace);
}
@Override
public Class<?> getObjectType() {
return managerInterFace;
}
@Override
public boolean isSingleton() {
return true;
}
}
8.代理工厂类
public class JdkProxyManagerFactory {
private static Map<String, Object> cache = new ConcurrentHashMap<>();
public Object getManager(Class managerInterface) {
try {
String managerInterfaceName = managerInterface.getName();
Object cacheObj = cache.get(managerInterfaceName);
if (cacheObj != null) {
return cacheObj;
}
Field sqlServerMapperField = managerInterface.getDeclaredField("sqlServerMapper");
Field mysqlMapperField = managerInterface.getDeclaredField("mysqlMapper");
Field switchKeyField = managerInterface.getDeclaredField("switchKey");
sqlServerMapperField.setAccessible(true);
Class sqlServerMapperVal = (Class) sqlServerMapperField.get(managerInterface);
mysqlMapperField.setAccessible(true);
Class mysqlMapperVal = (Class) mysqlMapperField.get(managerInterface);
switchKeyField.setAccessible(true);
String switchKeyVal = (String) switchKeyField.get(managerInterface);
String shortSqlServerClassName = ClassUtils.getShortName(sqlServerMapperVal.getSimpleName());
String sqlserverMapperBeanName = Introspector.decapitalize(shortSqlServerClassName);
String shortMysqlClassName = ClassUtils.getShortName(mysqlMapperVal.getSimpleName());
String mysqlMapperBeanName = Introspector.decapitalize(shortMysqlClassName);
Object proxy = Proxy.newProxyInstance(managerInterface.getClassLoader(), new Class[]{managerInterface}, new JdkProxyManagerHandler(sqlserverMapperBeanName, mysqlMapperBeanName, switchKeyVal));
cache.put(managerInterfaceName, proxy);
return proxy;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
9.核心来了,代理处理类,根据开关决定获取什么Bean,调用spring 的ReflectionUtils.invokeMethod执行。看代码可以发现支持双读,双写,通过配置的顺序决定最后返回mysql还是sqlserver结果。
@Slf4j
public class JdkProxyManagerHandler implements InvocationHandler {
private final String sqlserverMapperBeanName;
private final String mysqlMapperBeanName;
private final String switchKeyTableName;
private Object sqlserverBean;
private Object mysqlBean;
private SwitchConfigProps switchConfigProps;
public JdkProxyManagerHandler(String sqlserverMapperBeanName, String mysqlMapperBeanName, String switchKeyTableName) {
this.sqlserverMapperBeanName = sqlserverMapperBeanName;
this.mysqlMapperBeanName = mysqlMapperBeanName;
this.switchKeyTableName = switchKeyTableName;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (sqlserverBean == null) {
sqlserverBean = SpringUtils.getBean(sqlserverMapperBeanName);
}
if (mysqlBean == null) {
mysqlBean = SpringUtils.getBean(mysqlMapperBeanName);
}
if (switchConfigProps == null) {
switchConfigProps = SpringUtils.getBeanFirst(SwitchConfigProps.class);
}
// 根据switchKeyTableName 得到
String switchKeyVal = Optional.ofNullable(switchConfigProps)
.map(SwitchConfigProps::getTableSwitch)
.map(m -> m.get(switchKeyTableName))
.orElse(null);
log.info("================switchKeyVal:{}================", switchKeyVal);
// 1是默认(sqlserver)
if (switchKeyVal == null || "1".equals(switchKeyVal)) {
// 注解 走sqlserver
return ReflectionUtils.invokeMethod(method, sqlserverBean, args);
} else if ("2".equals(switchKeyVal)) {
// 注解走mysql
return ReflectionUtils.invokeMethod(method, mysqlBean, args);
} else if ("1,2".equals(switchKeyVal)) {
// sqlserver报错,就直接返回错误即可,不必加try-catch
// 注解 走sqlserver
ReflectionUtils.invokeMethod(method, sqlserverBean, args);
// 注解走mysql
// 返回mysql数据
return ReflectionUtils.invokeMethod(method, mysqlBean, args);
} else if ("2,1".equals(switchKeyVal)) {
//log.info("===========走进来了=============");
// 注解 走sqlserver, 一定要先走!!!
Object result = ReflectionUtils.invokeMethod(method, sqlserverBean, args);
try {
// 注解走mysql, 一定要后走!!!
ReflectionUtils.invokeMethod(method, mysqlBean, args);
} catch (Throwable ex) {
log.error("双写mysql异常:" + ex.getMessage(), ex);
}
// 返回sqlserver 数据
return result;
}
throw new RuntimeException("switchKey 库开关不正确 switchKeyTableName==>" + switchKeyTableName + ":" + switchKeyVal);
}
}
spring工具类
@Slf4j
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static <T> T getBean(String beanName) {
if (applicationContext == null) {
log.warn("name !! application context null !!!!!");
return null;
}
Object result = applicationContext.getBean(beanName);
if (result != null) {
return (T) result;
}
return null;
}
public static <T> T getBeanFirst(Class<T> requiredType) {
if (applicationContext == null) {
log.warn("type !! application context null !!!!!");
return null;
}
Object result = applicationContext.getBean(requiredType);
if (result != null) {
return (T) result;
}
return null;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
}
觉得麻烦就用最简单的if...else,不喜欢勿喷!!!