数据库双写

3 篇文章 0 订阅
1 篇文章 0 订阅

        公司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,不喜欢勿喷!!!

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值