栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

数据库双写

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

数据库双写

        公司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 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 sqlServerMapper = AlbAlbumMapper.class;
    Class mysqlMapper = MysqlAlbAlbumMapper.class;
    String switchKey = "ibt_alb_album";
}

3.编写开关配置类

@Data
@RefreshScope
@ConfigurationProperties(prefix = "switch.config")
public class SwitchConfigProps {

    private Map 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 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 doScan(String... basePackages) {
        return super.doScan(basePackages);
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }
}

7.创建代理类

public class JdkManagerFactoryBean implements FactoryBean {

    private Class managerInterFace;

    public void setManagerInterFace(Class managerInterFace) {
        this.managerInterFace = managerInterFace;
    }

    public JdkManagerFactoryBean(Class 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 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 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 getBeanFirst(Class 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,不喜欢勿喷!!!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1032176.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号