Spring环境下MyBatis支持多个Datasource参考实现

需求背景

最近接到一个项目,需要改造一个老的系统。该老系统以Oracle为存储,巨量的PL/SQL代码实现业务代码,C实现Socket Server,作为Client和PL/SQL的桥梁。不出所料,该老系统最大的问题是PL/SQL代码量巨大(上万的Procedure好几个),且毫无组织可言,实在改不动了,其次是性能有问题。改动的方向是,把PL/SQL从Oracle中踢出,用Java改写相关业务逻辑,放到Web Server中,不过Oracle中的Schema不动。

到目前位置,改造老系统和笔者要分享的主题没啥关系。问题来了,老系统有三套,A,B,C,就是说有三个Oracle数据库,Schema以及PL/SQL完全相同,但是数据没有啥关系,完全独立的三个数据库。历史的问题,不好评说,但是只是为了解决性能问题,搞了三套,客户被分配到不同的套系统,和传统的游戏开个服务器一个思路。

我们有3个方案:
1. 部署三套WebServer,对应三个Oracle数据库,客户端连接到不同的Web Server,和原来架构相同。数据库和Server都继续分。
2. 把三个数据库整合为一个数据库,部署一套Web Server。数据和,Server和。
3. 保持三套Oracle数据库不变,一个Web Server,但是Server需要把三个Oracle都管理起来。

方案1最简单,最容易,但是当性能已经不再是问题的时候还是部署三套Web Server,实在是有些说不过去,运维工作增加,客户端维护不同的版本(服务器地址),不太愿意选择,暂时备选。

方案2很难,非常的难。原来的三个Oracle数据库,完全独立,没有全局的主键,基本上无法区分开数据。放弃!

方案3是个折中的方案,中国人讲究中庸之道。最终我们选择了方案3。

设计思路

方案3要解决的问题是同一个Server如何集成3个数据库,具体来说,就是Spring里面如何管理3个Shema完全相同的Datasouce。我们的系统Server的技术选型是常见的Spring+MyBatis。管理多个Shema不同的Datasouce,网上有很多例子,Schema相同这叫分库吗,貌似很少?那进一步在Spring环境下呢?没有找到。强调Spring只想知道一个Mapper,而非3个。是因为,Spring只想知道一个Mapper,而非3个。

其中的关键技术难点如下:

  1. 如何识别什么样的数据应该存到哪一个Oracle数据库?
    我们的解法是根据用户所在的位置来判断,用户在A库,那么后续的操作针对A库,在B库就操作B库。最开始登录的时候先探测用户到底存在于哪一个库。我们认为用户名+密码应该是跨三个数据库唯一的
  2. 如何动态切换数据库?
    代理,用代理来实现。Spring容器内注册的就是一个代理而已,代理被调用的时候,我们截获调用,根据登录时候获得的环境(哪一个库),来动态切换,委托给背后的MyBatis Mapper来执行。

下图是我画的切换的示意图。偷懒的原因,我只画了A,B两套,实战中是A,B,C。

使用代理实现动态切换

实例代码

识别应该存于哪一个数据库

稍微改造一下Shiro访问用户信息的地方,增加环境的属性。注意两点,一是遍历数据源,探测用户所在的数据库,而是直接用SqlSessionFactory的bean name作为环境名,够简单直白!

Shiro Realm代码如下:

public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private SecureService secureService;
    @Autowired
    private AuthorizationService authorizationService;

    @PostConstruct
    public void initAlgorithms() {
        AllowAllCredentialsMatcher credentialsMatcher = new AllowAllCredentialsMatcher();
        setCredentialsMatcher(credentialsMatcher);
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken pairToken = (UsernamePasswordToken) token;
        String userName = pairToken.getUsername();
        String password = new String(pairToken.getPassword());

        // determine env
        Map<String, Object> envMappers = EnvMapperFactoryBean.getAllMappers(UserMapper.class);
        for (String env : envMappers.keySet()) {
            User user = validUser((UserMapper) envMappers.get(env), userName, password);
            if (user != null) {
                ShiroUser shiroUser = new ShiroUser(user.getId(), userName, user.getBranchId(), env);

                String salt = user.getSalt();
                byte[] saltBytes = Hex.decode(salt);

                return new SimpleAuthenticationInfo(shiroUser, user.getPassword(), ByteSource.Util.bytes(saltBytes),
                        getName());
            }
        }

        return null;
    }

    private User validUser(UserMapper userMapper, String userName, String password) {
        UserExample example = new UserExample();
        example.createCriteria().andNameEqualTo(userName);

        List<User> users = userMapper.selectByExample(example);
        if (users.isEmpty()) {
            return null;
        }

        User user = users.get(0);
        String salt = user.getSalt();
        String tempEncoded = secureService.hash(password, salt);
        if (user.getPassword().equals(tempEncoded)) {
            return user;
        } else {
            return null;
        }
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        User user = userService.queryUserByName(shiroUser.getName());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        List<Role> roles = authorizationService.queryRoleByUserId(user.getId());
        if (roles.size() > 0) {
            List<String> roleNames = ObjectProcessor.getFieldList(roles,
                    new ObjectProcessor.FieldValueGetter<Role, String>() {
                        @Override
                        public String getValue(Role role) {
                            return role.getName();
                        }
                    });
            info.addRoles(roleNames);

            List<Integer> roleIds = ObjectProcessor.getFieldList(roles,
                    new ObjectProcessor.FieldValueGetter<Role, Integer>() {
                        @Override
                        public Integer getValue(Role role) {
                            return role.getId();
                        }
                    });
            List<Privilege> privileges = authorizationService.queryPrivilegeByRoleId(roleIds);
            if (privileges.size() > 0) {
                List<String> privilegeKeys = ObjectProcessor.getFieldList(privileges,
                        new ObjectProcessor.FieldValueGetter<Privilege, String>() {
                            @Override
                            public String getValue(Privilege item) {
                                return item.getCategory() + ":" + item.getCode();
                            }
                        });
                info.addStringPermissions(privilegeKeys);
            }
        }

        return info;
    }

    public static class ShiroUser implements Serializable, Principal {
        private static final long serialVersionUID = 3316911162161110480L;

        private Integer id;
        private String name;
        private Integer branchId;
        private String env;

        public ShiroUser(Integer id, String name, Integer branchId, String env) {
            this.id = id;
            this.name = name;
            this.branchId = branchId;
            this.env = env;
        }

        public Integer getId() {
            return id;
        }

        public String getName() {
            return name;
        }

        public Integer getBranchId() {
            return branchId;
        }

        public String getEnv() {
            return env;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            ShiroUser other = (ShiroUser) obj;
            if (name == null) {
                if (other.name != null) {
                    return false;
                }
            } else if (!name.equals(other.name)) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            return result;
        }

        @Override
        public String toString() {
            return name;
        }
    }

}

Spring Mapper Proxy注册以及动态切换数据源

我们用了Shiro,可以在任意地方获取用户信息,背后的本质是一个ThreadLocal变量。注意Proxy的背后会有很多帮工—-真正的Mybatis Mapper,启动的时候需要安装上。

代码如下:
public class EnvMapperFactoryBean implements FactoryBean, ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(EnvMapperFactoryBean.class);

    private Class<T> mapperInterface;
    private ApplicationContext context;

    private static Map<String, Map<String, Object>> envMappers = new ConcurrentHashMap<>();

    /**
     * Sets the mapper interface of the MyBatis mapper
     *
     * @param mapperInterface
     *            class of the interface
     */
    public void setMapperInterface(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public T getObject() throws Exception {
        installEnv();

        return (T) Proxy.newProxyInstance(EnvMapperFactoryBean.class.getClassLoader(),
                new Class<?>[] { mapperInterface }, new MapperProxy());
    }

    public Class<T> getObjectType() {
        return this.mapperInterface;
    }

    public boolean isSingleton() {
        return true;
    }

    private static Object getRealMapper(String env, Class mapperClazz) {
        Map<String, Object> mappers = envMappers.get(env);
        if (mappers.isEmpty()) {
            return null;
        }

        return mappers.get(mapperClazz.getName());
    }

    public static Map<String, Object> getAllMappers(Class mapperClazz) {
        Map<String, Object> result = new HashMap<>();
        String clazzName = mapperClazz.getName();

        for (String env : envMappers.keySet()) {
            Map<String, Object> mappers = envMappers.get(env);
            if (mappers.containsKey(clazzName)) {
                result.put(env, mappers.get(clazzName));
            }
        }

        return result;
    }

    @SuppressWarnings("resource")
    private void installEnv() {
        String[] sqlSessionFactoryNames = context.getBeanNamesForType(SqlSessionFactory.class);
        if (sqlSessionFactoryNames == null || sqlSessionFactoryNames.length == 0) {
            throw new RuntimeException("找不到SqlSessionFactory的配置信息");
        }

        for (String env : sqlSessionFactoryNames) {
            SqlSessionFactory sqlSessionFactory = context.getBean(env, SqlSessionFactory.class);
            SqlSession sqlSession = new SqlSessionTemplate(sqlSessionFactory);
            T mapper = sqlSession.getMapper(mapperInterface);

            if (!envMappers.containsKey(env)) {
                envMappers.put(env, new ConcurrentHashMap<>());
            }

            envMappers.get(env).put(mapperInterface.getName(), mapper);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    private class MapperProxy implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object object = null;
            try {
                ShiroUser shiroUser = (ShiroUser) SecurityUtils.getSubject().getPrincipal();
                if (shiroUser == null) {
                    throw new RuntimeException("用户没有登陆,无法确认环境");
                }

                String env = shiroUser.getEnv();

                Object mapper = getRealMapper(env, method.getDeclaringClass());
                if (mapper == null) {
                    throw new RuntimeException("找不到对应的mapper");
                }
                object = method.invoke(mapper, args);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw e;
            }
            return object;
        }
    }
}

Spring的配置文件

Spring会配置多个Datasource,多个SqlSessionFactory,一个Scanner。

<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource"
    destroy-method="close">
    <!-- Connection Info -->
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />

    <!-- Connection Pooling Info -->
    <property name="maxActive" value="${jdbc.pool.maxActive}" />
    <property name="maxIdle" value="${jdbc.pool.maxIdle}" />
    <property name="minIdle" value="0" />
    <property name="defaultAutoCommit" value="false" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="typeAliasesPackage" value="com.comstar.mars.entity" />
    <property name="mapperLocations" value="classpath:/mybatis/*Mapper.xml" />
</bean>

<bean id="dataSource1" class="org.apache.tomcat.jdbc.pool.DataSource"
    destroy-method="close">
    <!-- Connection Info -->
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url.moon}" />
    <property name="username" value="${jdbc.username.moon}" />
    <property name="password" value="${jdbc.password.moon}" />

    <!-- Connection Pooling Info -->
    <property name="maxActive" value="${jdbc.pool.maxActive}" />
    <property name="maxIdle" value="${jdbc.pool.maxIdle}" />
    <property name="minIdle" value="0" />
    <property name="defaultAutoCommit" value="false" />
</bean>

<bean id="sqlSessionFactory1" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource1" />
    <property name="typeAliasesPackage" value="com.comstar.mars.entity" />
    <property name="mapperLocations" value="classpath:/mybatis/*Mapper.xml" />
</bean>

<bean class="com.comstar.mars.env.EnvMapperScannerConfigurer">
    <property name="basePackage" value="com.comstar.mars.repository" />
</bean>

结语

这是一个通过代理器来实现运行时动态切换实现的经典案例,代理是一个非常有用的设计模式,值得思考和借鉴。

有人会问,有其它的一些解法吗?

关于识别,用户名+密码唯一识别有人可能觉得不妥,可以自己替换为自己想要的识别算法,甚至于丢给客户端自己决定。

关于动态切换,Spring的AbstractRoutingDataSource是个很好的选择,不过不适合我们的场景,我们需要先拿到三个Mybatis的UserMapper来探测具体用哪一个数据库,Datasource太底层了。如果环境由客户端来决定,AbstractRoutingDataSource确实是更好的选择。

Github地址

https://github.com/kimylrong/multi-datasource.git

友情链接

书法和国画爱好者请进

猜你喜欢

转载自blog.csdn.net/kimylrong/article/details/50471097