成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

聊聊 SaaS 多租戶系統(tǒng)數(shù)據(jù)隔離實(shí)現(xiàn)方案

云計(jì)算 SaaS
不同角色只能看到不同數(shù)據(jù),部門經(jīng)理只能看到自己部門的數(shù)據(jù),小組長(zhǎng)只能看到自己小組的數(shù)據(jù),這些實(shí)現(xiàn)邏輯套路都可以模仿多租戶插件的方式進(jìn)行優(yōu)雅實(shí)現(xiàn),這也是我后面有時(shí)間想研究的,后續(xù)會(huì)再出一篇數(shù)據(jù)權(quán)限的實(shí)現(xiàn)方案總結(jié)。

開發(fā)過(guò)SaaS系統(tǒng)平臺(tái)的小伙伴一定對(duì)多租戶這個(gè)概念不陌生,簡(jiǎn)單來(lái)說(shuō)一個(gè)租戶就是一個(gè)公司客戶,多個(gè)租戶共用同一個(gè)SaaS系統(tǒng),一旦SaaS系統(tǒng)不可用,那么所有的租戶都不可用。你可以這么理解SaaS系統(tǒng)就像一棟大樓,而租戶就是大樓里面租辦公樓層的公司,平時(shí)每家公司做著自己的業(yè)務(wù),互不干擾,但是一旦大樓的電梯壞了,那么影響到的就是所有的公司。

多租戶問(wèn)題,其是一種架構(gòu)設(shè)計(jì)方式,就是在一臺(tái)或者一組服務(wù)器上運(yùn)行的SaaS系統(tǒng),可以為多個(gè)租戶(客戶)提供服務(wù),目的是為了讓多個(gè)租戶在互聯(lián)網(wǎng)環(huán)境下使用同一套程序,且保證租戶間的數(shù)據(jù)隔離。從這種架構(gòu)設(shè)計(jì)的模式上,不難看出來(lái),多租戶架構(gòu)的重點(diǎn)就是同一套程序下多個(gè)租戶數(shù)據(jù)的隔離。由于租戶數(shù)據(jù)是集中存儲(chǔ)的,所以要實(shí)現(xiàn)數(shù)據(jù)的安全性,就是看能否實(shí)現(xiàn)對(duì)租戶數(shù)據(jù)的隔離,防止租戶數(shù)據(jù)不經(jīng)意或被他人惡意地獲取和篡改。在講多租戶數(shù)據(jù)隔離實(shí)現(xiàn)之前,先來(lái)看看什么是SaaS系統(tǒng)。

什么是SaaS系統(tǒng)?

SaaS平臺(tái)是運(yùn)營(yíng)saas軟件的平臺(tái)。SaaS提供商為企業(yè)搭建信息化所需要的所有網(wǎng)絡(luò)基礎(chǔ)設(shè)施及軟件、硬件運(yùn)作平臺(tái),并負(fù)責(zé)所有前期的實(shí)施、后期的維護(hù)等一系列服務(wù),租戶(企業(yè))無(wú)需購(gòu)買軟硬件、建設(shè)機(jī)房、招聘IT人員,即可通過(guò)互聯(lián)網(wǎng)使用信息系統(tǒng)。SaaS 是一種軟件布局模型,其應(yīng)用專為網(wǎng)絡(luò)交付而設(shè)計(jì),便于用戶通過(guò)互聯(lián)網(wǎng)托管、部署及接入。

簡(jiǎn)單來(lái)說(shuō)就是租戶給SaaS平臺(tái)付租金就能使用平臺(tái)提供的功能服務(wù),當(dāng)下比較典型就是各種云平臺(tái)、云服務(wù)廠商。

多租戶數(shù)據(jù)隔離架構(gòu)設(shè)計(jì)

目前saas多租戶系統(tǒng)的數(shù)據(jù)隔離有三種架構(gòu)設(shè)計(jì),即為每個(gè)租戶提供獨(dú)立的數(shù)據(jù)庫(kù)、獨(dú)立的表空間、按字段區(qū)分租戶,每種方案都有其各自的適用情況。

一個(gè)租戶獨(dú)立一個(gè)數(shù)據(jù)庫(kù)

一個(gè)租戶獨(dú)立使用一個(gè)數(shù)據(jù)庫(kù),那就意味著我們的SaaS系統(tǒng)需要連接多個(gè)數(shù)據(jù)庫(kù),這種實(shí)現(xiàn)方案其實(shí)就和分庫(kù)分表架構(gòu)設(shè)計(jì)是一樣的,好處就是數(shù)據(jù)隔離級(jí)別高、安全性好,畢竟一個(gè)租戶單用一個(gè)數(shù)據(jù)庫(kù),但是物理硬件成本,維護(hù)成本也變高了。

獨(dú)立的表空間

這種方案的實(shí)現(xiàn)方式,就是所有租戶共用一個(gè)數(shù)據(jù)庫(kù)系統(tǒng),但是每個(gè)租戶在數(shù)據(jù)庫(kù)系統(tǒng)中擁有一個(gè)獨(dú)立的表空間。

按租戶id字段隔離租戶

這種方案是多租戶方案中最簡(jiǎn)單的數(shù)據(jù)隔離方法,即在每張表中都添加一個(gè)用于區(qū)分租戶的字段(如tenant_id或org_id啥的)來(lái)標(biāo)識(shí)每條數(shù)據(jù)屬于哪個(gè)租戶,當(dāng)進(jìn)行查詢的時(shí)候每條語(yǔ)句都要添加該字段作為過(guò)濾條件,其特點(diǎn)是所有租戶的數(shù)據(jù)全都存放在同一個(gè)表中,數(shù)據(jù)的隔離性是最低的,完全是通過(guò)字段來(lái)區(qū)分的,很容易把數(shù)據(jù)搞串或者誤操作。

三種數(shù)據(jù)隔離架構(gòu)設(shè)計(jì)的對(duì)比如下:

隔離方案

成本

支持租戶數(shù)量

優(yōu)點(diǎn)

缺點(diǎn)

獨(dú)立數(shù)據(jù)庫(kù)系統(tǒng)

數(shù)據(jù)隔離級(jí)別高,安全性,可以針對(duì)單個(gè)租戶開發(fā)個(gè)性化需求

數(shù)據(jù)庫(kù)獨(dú)立安裝,物理成本和維護(hù)成本都比較高

獨(dú)立的表空間

較多

提供了一定程度的邏輯數(shù)據(jù)隔離,一個(gè)數(shù)據(jù)庫(kù)系統(tǒng)可支持多個(gè)租戶

數(shù)據(jù)庫(kù)管理比較困難,表繁多,同時(shí)數(shù)據(jù)修復(fù)稍復(fù)雜

按租戶id字段區(qū)分

維護(hù)和購(gòu)置成本最低,每個(gè)數(shù)據(jù)庫(kù)能夠支持的租戶數(shù)量最多

隔離級(jí)別最低,安全性也最低

大部分公司都是采用第三種:按租戶id字段隔離租戶架構(gòu)設(shè)計(jì)實(shí)現(xiàn)多租戶數(shù)據(jù)隔離的。接下來(lái)我們就來(lái)看看代碼層面怎么實(shí)現(xiàn)多租戶數(shù)據(jù)隔離的。關(guān)注公眾號(hào):碼猿技術(shù)專欄,回復(fù)關(guān)鍵詞:1111 獲取阿里內(nèi)部Java性能調(diào)優(yōu)手冊(cè)!

mybatis-plus優(yōu)雅實(shí)現(xiàn)多租戶數(shù)據(jù)權(quán)限隔離

上面我們說(shuō)過(guò)按租戶id字段隔離租戶這種方式就是在獲取數(shù)據(jù)的時(shí)候?qū)γ恳粭lSQL語(yǔ)句添加租戶id作為過(guò)濾條件來(lái)隔離租戶數(shù)據(jù)的。但是這樣意味著每個(gè)查詢SQL都必須加上租戶id這個(gè)過(guò)濾條件,如果漏加就意味著會(huì)查詢出不同租戶的數(shù)據(jù),這是絕對(duì)不允許的,同時(shí)每個(gè)查詢接口都需要手動(dòng)設(shè)置過(guò)濾條件,重復(fù)勞動(dòng),一點(diǎn)都不夠優(yōu)雅。這時(shí)候就不得不說(shuō)說(shuō)mybatis-plus的多租戶插件了,看看它如何優(yōu)雅實(shí)現(xiàn)多租戶隔離的?再講述之前,我們先思考一下如何優(yōu)雅實(shí)現(xiàn)數(shù)據(jù)隔離?首先我們要求每一條SQL都加上租戶id這個(gè)過(guò)濾條件,這意味著我們需要解析原始SQL在合適的地方加上租戶id過(guò)濾條件,我們知道m(xù)ybatis提供擴(kuò)展點(diǎn)就是攔截器,可以對(duì)SQL語(yǔ)句處理前后進(jìn)行增強(qiáng)邏輯,分頁(yè)插件就是這么做的,所以我們這里要增強(qiáng)SQL自然也是這樣,接下來(lái)我們就來(lái)看看mybatis-plus多租戶插件是怎么實(shí)現(xiàn)多租戶數(shù)據(jù)隔離的,插件官網(wǎng)介紹地址:https://www.baomidou.com/pages/aef2f2/#tenantlineinnerinterceptor,該攔截器部分源碼如下:

public class TenantLineInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
    // 多租戶處理器
    private TenantLineHandler tenantLineHandler;

    // 改SQL,添加多租戶id條件
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        if (!InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
            MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
            mpBs.sql(this.parserSingle(mpBs.sql(), (Object)null));
        }
    }

    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
                return;
            }

            MPBoundSql mpBs = mpSh.mPBoundSql();
            mpBs.sql(this.parserMulti(mpBs.sql(), (Object)null));
        }

    }
  
  // 礙于篇幅問(wèn)題,下面省略的代碼就是繼承抽象類JsqlParserSupport解析SQL然后添加多租戶id條件的,可以自行查看源碼
  ......
}

接著我們來(lái)看看處理器TenantLineHandler,這是一個(gè)接口,需要我們提供自定義實(shí)現(xiàn),指定多租戶相關(guān)配置:

public class TenantDatabaseHandler implements TenantLineHandler {
    private final Set<String> ignoreTables = new HashSet<>();

    public TenantDatabaseHandler(TenantProperties properties) {
        // 將配置文件配置的忽略表名同步大小寫,適配不同寫法
        properties.getIgnoreTables().forEach(table -> {
            ignoreTables.add(table.toLowerCase());
            ignoreTables.add(table.toUpperCase());
        });
    }

    /**
     * 獲取租戶字段名
     * <p>
     * 默認(rèn)字段名叫: tenant_id,我這里使用org_id
     *
     * @return 租戶字段名
     */
    @Override
     public String getTenantIdColumn() {
        return "org_id";
    }


    @Override
    public Expression getTenantId() {
        // 這里通過(guò)登錄信息上下文返回租戶id給多租戶攔截器增強(qiáng)SQL使用
        return new LongValue(RequestUserHolder.getCurrentUser().getOrgId());
    }

    @Override
    public boolean ignoreTable(String tableName) {
        // 忽略多租戶的表
        return CollUtil.contains(ignoreTables, tableName);
    }
}

配置屬性如下:

@ConfigurationProperties(prefix = "ptc.tenant")
@Data
public class TenantProperties {


    /**
     * 全局控制是否開啟多租戶功能
     */
    private Boolean enable = Boolean.TRUE;

    /**
     * 需要忽略多租戶的表
     *
     * 即默認(rèn)所有表都開啟多租戶的功能,所以記得添加對(duì)應(yīng)的 tenant_id 字段喲
     */
    private Set<String> ignoreTables = Collections.emptySet();
}

接下來(lái)注入攔截器插件即可:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        // 必須保證多租戶插件在分頁(yè)插件之前,這個(gè)是 MyBatis-plus 的規(guī)定
        if (properties.getEnable()) {
            mybatisPlusInterceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantDatabaseHandler(properties)));
        }
        // 分頁(yè)插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }

使用示例如下:這里提供了一個(gè)常見的案例:用戶和角色關(guān)聯(lián)查詢的SQL:getUserList()

<select id="getUserList" resultType="com.plasticene.textile.entity.User">
        select u.* from user u
        left join user_role r on u.id = r.user_id
        <where>
            <if test="query.status != null">
                and u.status = #{query.status}
            </if>
            <if test="query.roleId != null">
                and r.role_id = #{query.roleId}
            </if>
            <if test="query.keyword != null">
                and ((u.name like concat('%',#{query.keyword},'%')) or (u.mobile like concat(#{query.keyword},'%')))
            </if>
            <if test="query.startEntryTime != null">
                and u.entry_time >= #{query.startEntryTime}
            </if>
            <if test="query.endEntryTime != null">
                <![CDATA[ and u.entry_time <= #{query.endEntryTime}]]>
            </if>

        </where>
        group by u.id
        order by u.id desc
    </select>

啟動(dòng)項(xiàng)目,先登錄之后使用token掉接口執(zhí)行下面代碼邏輯:

public PageResult<UserDTO> getList(UserQuery query) {
        Page<UserDTO> page = new Page<>(query.getPageNo(), query.getPageSize());
        List<User> userList = userDAO.getUserList(page, query);
        List<UserDTO> userDTOS = toUserDTOList(userList);
        return new PageResult<>(userDTOS, page.getTotal(), page.getPages());
    }

查看控制臺(tái)發(fā)現(xiàn):

[1658720355293990912] [DEBUG] [2023-05-17 14:25:25.504] [http-nio-16688-exec-1@23652]  com.plasticene.textile.dao.UserDAO.getUserList debug : ==>  Preparing: SELECT u.* FROM user u LEFT JOIN user_role r ON u.id = r.user_id AND r.org_id = 3 WHERE u.org_id = 3 GROUP BY u.id ORDER BY u.id DESC LIMIT ?
[1658720355293990912] [DEBUG] [2023-05-17 14:25:25.505] [http-nio-16688-exec-1@23652]  com.plasticene.textile.dao.UserDAO.getUserList debug : ==> Parameters: 20(Long)

user表u加上u.org_id=3這個(gè)多租戶過(guò)濾條件,user_role也同樣加上了,說(shuō)明多租戶插件起作用了。

當(dāng)然如果想忽略掉表user,我們只需要在配置文件如下配置即可:

ptc:
  tenant:
    ignore-tables: user

這樣user表u就不會(huì)再加上u.org_id=3這個(gè)多租戶過(guò)濾條件,但是這里有一個(gè)細(xì)節(jié)需要注意,由于user在MySQL中是關(guān)鍵字,所以我有時(shí)候?yàn)榱艘?guī)范書寫SQL,會(huì)按照如下編寫:

select u.* from `user` u
        left join user_role r on u.id = r.user_id

這時(shí)候你會(huì)發(fā)現(xiàn)上面配置的忽略表user不起作用,還是會(huì)加上u.org_id=3這個(gè)多租戶過(guò)濾條件,跟源碼才發(fā)現(xiàn)我們上面自定義的多租戶處理器TenantLineHandler只對(duì)表名進(jìn)行了大小寫適配,然而這里SQL解析出來(lái)的表名是: **user**,所以匹配不到配置不起作用。

當(dāng)然我們有可能需要針對(duì)單一SQL語(yǔ)句不加多租戶過(guò)濾條件,可以使用@InterceptorIgnore注解:

public interface UserDAO extends BaseMapperX<User> {

    @InterceptorIgnore(tenantLine = "true")
    List<User> getUserList(IPage<UserDTO> userPage, @Param("query") UserQuery query);
}

這樣調(diào)用getUserList()不再會(huì)加多租戶過(guò)濾條件了。

通過(guò)上面我們知道了這個(gè)多租戶插件其實(shí)就是通過(guò)解析SQL,然后進(jìn)行拼接多租戶id過(guò)濾條件來(lái)實(shí)現(xiàn)SQL增強(qiáng)從而做到數(shù)據(jù)隔離,解析SQL的框架叫:JSqlParser,官方文檔:https://github.com/JSQLParser/JSqlParser/wiki,之前我總結(jié)過(guò)一篇關(guān)于 Druid解析動(dòng)態(tài)SQL。Druid也可以解析SQL,我們都知道SQL語(yǔ)句會(huì)生成語(yǔ)法樹,兩者對(duì)SQL解析的孰強(qiáng)孰弱(特別是復(fù)雜SQL)不得而知,可以自行驗(yàn)證對(duì)比,我這里給出一個(gè)JSqlParser解析出錯(cuò)的情況,把上面的SQL語(yǔ)句user_role r 改為 user_role ur

select u.* from user u
        left join user_role ur on u.id = ur.user_id

按照上面一樣調(diào)用執(zhí)行g(shù)etUserList(), 會(huì)報(bào)解析錯(cuò)誤:

Caused by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select u.* from user u
left join user_role ur on u.id = ur.user_id
group by u.id
order by u.id desc
at com.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:39)
at com.baomidou.mybatisplus.extension.parser.JsqlParserSupport.parserSingle(JsqlParserSupport.java:52)
at com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.beforeQuery(TenantLineInnerInterceptor.java:65)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:78)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62)
at com.sun.proxy.$Proxy178.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
... 101 common frames omitted
Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "ur" <K_ISOLATION>
at line 2, column 29.

我在mybatis-plus的官方提了一個(gè)issue:https://github.com/baomidou/mybatis-plus/issues/5086,也得到官方維護(hù)者的迅速回應(yīng)說(shuō)是JSqlParser解析的問(wèn)題,不是mybatis-plus的問(wèn)題~~~,給出的建議就是把別名ur改成別的,或者升級(jí)到JSqlParser的最新版本。

4.總結(jié)

至此,我們對(duì)多租戶系統(tǒng)數(shù)據(jù)隔離實(shí)現(xiàn)方案,架構(gòu)設(shè)計(jì),以及如何優(yōu)雅實(shí)現(xiàn)全局操作數(shù)據(jù)隔離都講完了,同時(shí)也對(duì)mybati-plus的多租戶插件實(shí)現(xiàn)原理和源碼流程套路進(jìn)行了淺析,也對(duì)實(shí)際應(yīng)用案例中進(jìn)行了舉證并闡述了相關(guān)細(xì)節(jié)點(diǎn)。當(dāng)然數(shù)據(jù)權(quán)限不止停留在租戶(公司)層面上面,大多數(shù)系統(tǒng)的數(shù)據(jù)權(quán)限會(huì)按照業(yè)務(wù)組織架構(gòu)角色來(lái)控制,數(shù)據(jù)權(quán)限其套路和根據(jù)角色判斷菜單權(quán)限一回事。由于數(shù)據(jù)權(quán)限通常與公司業(yè)務(wù)相關(guān),比較個(gè)性化,每家公司業(yè)務(wù)組織架構(gòu)不盡相同,所以實(shí)際開發(fā)項(xiàng)目的數(shù)據(jù)權(quán)限隔離還需要大家按實(shí)際需求進(jìn)行修改,但總的來(lái)說(shuō)我們可以模仿多租戶隔離實(shí)現(xiàn)方式,比如說(shuō)一個(gè)業(yè)務(wù)系統(tǒng)組織架構(gòu)有公司(org_id),公司下有多個(gè)部門(dept_id),部門下有多個(gè)團(tuán)隊(duì)分組(team_id),團(tuán)隊(duì)下有多個(gè)人員(user_id)。不同角色只能看到不同數(shù)據(jù),部門經(jīng)理只能看到自己部門的數(shù)據(jù),小組長(zhǎng)只能看到自己小組的數(shù)據(jù),這些實(shí)現(xiàn)邏輯套路都可以模仿多租戶插件的方式進(jìn)行優(yōu)雅實(shí)現(xiàn),這也是我后面有時(shí)間想研究的,后續(xù)會(huì)再出一篇數(shù)據(jù)權(quán)限的實(shí)現(xiàn)方案總結(jié)。

責(zé)任編輯:武曉燕 來(lái)源: 碼猿技術(shù)專欄
相關(guān)推薦

2025-05-14 03:00:00

2015-08-12 15:46:02

SaaS多租戶數(shù)據(jù)存儲(chǔ)

2023-12-14 12:26:16

SaaS數(shù)據(jù)庫(kù)方案

2020-05-14 18:04:20

Spring BootSaaS平臺(tái)

2024-03-28 09:46:50

2023-11-06 08:26:11

Spring微服務(wù)架構(gòu)

2020-09-15 07:00:00

SaaS架構(gòu)架構(gòu)

2015-04-02 11:04:27

云應(yīng)用SaaSOFBIZ

2025-01-09 14:39:40

2024-05-28 08:17:54

2019-03-22 14:38:03

容器安全隔離

2023-12-15 07:34:56

分布式數(shù)據(jù)庫(kù)數(shù)據(jù)庫(kù)租戶軟件架構(gòu)

2025-02-18 16:27:01

2023-11-29 08:35:28

群多租戶ES運(yùn)維

2015-11-12 09:58:45

多租戶SaaS軟件架構(gòu)設(shè)計(jì)

2021-05-17 11:47:41

多租戶系統(tǒng)私有化

2020-10-16 08:57:51

云平臺(tái)之多租戶的實(shí)踐

2023-10-11 08:09:53

事務(wù)隔離級(jí)別

2022-09-13 07:14:29

云計(jì)算SaaS多租戶

2022-07-08 08:07:14

SpringJavaMail
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 欧美激情 一区 | 欧美日韩激情 | 国产精品69毛片高清亚洲 | av资源中文在线 | 久久久免费电影 | 国产高清视频在线观看 | 中文字幕精品一区 | 精品欧美一区二区精品久久久 | 可以免费看的毛片 | 成人免费视频网 | 99这里只有精品视频 | 91麻豆精品国产91久久久久久 | 欧美精品在线观看 | 羞视频在线观看 | 一区二区国产在线 | 日本亚洲一区二区 | 国产日韩欧美中文 | 99国内精品久久久久久久 | 在线视频一区二区三区 | 国产精品一区二区三区在线 | 欧美xxxⅹ性欧美大片 | 手机av在线 | 超碰伊人久久 | 污污免费网站 | 欧美一区2区三区4区公司 | 97人澡人人添人人爽欧美 | 国产成人精品在线 | 中文字幕一区二区三区在线乱码 | 国产永久免费 | 伊人精品在线 | 91精品国产91久久久久久 | 欧美日韩综合精品 | 欧美成人久久 | 中国91av| 久久性av| 久久大陆 | 成人国产免费视频 | 天天干视频| 欧美久久久久久久久 | 久草视频观看 | 午夜视频一区二区 |