1. 程式人生 > >Spring環境下MyBatis支援多個Datasource參考實現

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

友情連結

書法和國畫愛好者請進