工作实践:用户资源权限查询优化

问题

我负责的项目中,有一个需求:要按照"部门(dept)、地区(area)、员工类型(empType)、自定义分组(group)"这几个维度,来设置"文章(article)"的可见范围。判断条件如下:

ini 复制代码
flag = (dept & area & empType) || group

当 flag == true 时,员工在前台可以查看该文章。

为了实现这个权限判断的功能,最初的实现方式是:

  • 使用用户的(dept、area、empType)信息查询出有权限的文章ID集合 S1
  • 使用用户所属的分组(group)查询出有权限的文章ID集合 S2
  • S1 和 S2 取并集得到文章ID 集合 S3,使用 S3 查询文章表并分页

这种查询方案,能够满足业务需求,但是随着数据量的增加,性能会越来越差。

问题分析

除了文章表 article 以外,还有文章可见范围表(article_view_scope),该表主要有三个字段:

diff 复制代码
article_view_scope:
- article_id : 文章ID
- scope_type : 可见范围类型,有 department、empType、area
- scope_value : 可见范围的取值

查询文章ID

查询文章ID集合的SQL如下:

SQL 复制代码
select a.article_id
from article a
// 员工类型
inner join article_view_scope empvs on empvs.article_id = a.article_id and empvs.scope_type = 'empType'
and empvs.scope_value in
<foreach collection="empTypes" item="id" open="(" separator="," close=")">
    #{id}
</foreach>
// 地区
inner join article_view_scope areavs on areavs.article_id = a.article_id and areavs.scope_type = 'area'
and areavs.scope_value in
<foreach collection="cityCodes" item="id" open="(" separator="," close=")">
    #{id}
</foreach>
// 员工部门
inner join article_view_scope deptvs on deptvs.article_id = a.article_id and deptvs.scope_type = 'department'
and scope_value in
<foreach collection="deptIds" item="id" open="(" separator="," close=")">
    #{id}
</foreach>
where a.status = #{status} and a.deleted = 0;

该SQL是用 article 表对 article_view_scope 做了三次联表查询,伪代码如下:

ini 复制代码
article_iter = iterator_over article
article_row = article_iter.next
while article_iter  
    # 依次遍历每个 article,使用 article_id 作为参数,查询对应的 article_view_scope
    inner_iter1 = iterator over article_view_scope avs1 where avs1.article_id = article.id and avs1.scope_value in ('X1')
    inner_iter1_row = inner_iter1.next
    if inner_iter1_row
        inner_iter2 = iterator over article_view_scope avs2 where avs2.article_id = article.id and avs2.scope_value in ('X2')
        inner_iter2_row = inner_iter2.next
        if inner_iter2_row
            inner_iter3 = iterator over article_view_scope avs3 where avs3.article_id = article.id and avs3.scope_value in ('X3')
            inner_iter3_row = inner_iter3.next
            if inner_iter3_row
                output[article_row.id]
                inner_iter3_row = inner_iter3.next
            end
        end
    end
end

在上面的SQL中,需要做一个四层的循环遍历:

  • 最外层,遍历所有文章ID
  • 内层,使用 artilce id 和 用户身份信息查询 article_view_scope 表中的数据,因为 article_id 已经加了索引,所以内层查询 article_view_scope 很快

对该语句的 EXPLAIN 如下:

与上面的分析一致,内层联表查询都使用了索引,查询性能还可以。

第二步根据用户分组查询文章ID,SQL语句相似,这里不再介绍。

查询文章列表

使用文章ID批量查询文章:

SQL 复制代码
SELECT *
FROM article
WHERE deleted = 0 and `status` = 'online'
AND article_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
</foreach>
order by last_publish_at DESC LIMT X,10

当 文章 id 量较大的时候,批量查询不会使用索引,而是全表扫描。如下所示:

存在的问题

1、查询文章ID时,联表次数等于可见范围的参数个数,后续增加新的可见范围参数,需要再增加一次联表,随着文章数的增加和联表次数的增加,性能会越来越差。

2、大部分文章都是全部可见(占比 60%~70%),用文章ID批量查询,都没有使用到索引,是全表扫描,随着文章数的增加,查询性能也会越来越差。

为了解决存在的问题,有下面的两种优化方案。

优化方案

1、 使用 ES 或 mongodb 代替 mysql

  • 把数据可见范围数据同步到ES,用ES来做复杂条件查询
  • 使用 mongodb 保存文章及其可见范围,给可见范围增加索引,做复杂查询

2、 依然使用 mysql,记录每个可见范围关联的文章ID 和 排序条件,根据用户的信息分页查询关联的文章ID,再查询文章详情

方案1:

  • 引入es 和 mongodb 一劳永逸解决问题,实现更简单
  • 需要引入第三方组件,费用成本更高

方案2:

  • 实现比方案1复杂
  • 不需要引入第三方组件,费用成本更低

出于"用已有的方法、更低的成本"这一初衷,下面主要介绍方案2的设计与实现,并给出与之前方案的性能对比。

方案设计与实现

设计

因为 mysql 不支持像 ES 和 mongodb 那样的复杂查询,所以我们才把 文章 和 可见范围 分开存储,分别存储在两张表里(文章表 article、可见范围表 article_view_scope)。

查询文章ID

前面的查询逻辑是"找出哪些文章可以被用户查看",所以遍历 article 表,联表 article_view_scope,判断每个文章对用户是否可见。

实际上,我们要做的事情是:把用户信息作为关键字,查询满足条件的文章,类似于ES的倒排索引的"倒查"思路,(view_scope_type + view_scope_value) --> articleId。

因此,在查询文章ID的时候,SQL可以改写成如下方式:

SQL 复制代码
select article_id FROM article_view_scope
WHERE
(scope_type = 'department' and scope_value in
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
    #{deptId}
</foreach>
)
OR (scope_type = 'area' and scope_value in
<foreach collection="cityCodes" item="cityCode" open="(" separator="," close=")">
    #{cityCode}
</foreach>
)
OR (scope_type = 'empType' and scope_value in
<foreach collection="empTypes" item="empType" open="(" separator="," close=")">
    #{empType}
</foreach>
)
<if test="groupIds != null and groupIds.size > 0">
    OR (scope_type = 'group' and scope_value in
    <foreach collection="groupIds" item="groupId" open="(" separator="," close=")">
        #{groupId}
    </foreach>
    )
</if>

这里查询出的是用户只要满足一个条件就能查看的文章ID,然后在内存中对结果做 与操作 和 或操作,得到用户能查看的所有 文章ID集合。

给 scope_type 和 scope_value 增加索引后,EXPLAIN 如下:

之前的方案相比:

1、查询出的数据相同,都是用户能查看的所有文章ID集合

2、没有联表

查询文章列表

使用上面的方法查询出文章ID集合,如果直接用来查询文章详情,依然不会使用索引。所以,在上面查询出文章ID后,需要能够根据排序条件对文章ID进行排序和分页。

目前文章的排序SQL如下(top 是否置顶、top_sequence 置顶的顺序、last_publish_at 最后发布时间):

SQL 复制代码
order by `top` DESC,top_sequence ASC, last_publish_at DESC

可以把这几个参数添加到 article_view_scope 表中,可见范围是文章的一个 值对象 字段,在保存文章时 会同步更新 可见范围 的值,两者可以实时保持一致。

在倒查文章ID集合后,在内存中对文章ID做排序和分页,取出对应的文章ID子集,使用ID子集查询文章详情。

文章ID集合的大小,就是每一页的数据量(20条),查询文章详情时可以使用到索引,EXPLAIN 如下:

之前的方案相比:

1、查询文章列表时,可以使用索引,性能不会随着文章数的增加而下降,保持稳定

测试

主要是测试两种方案查询文章的性能,为了避免外部服务(从用户中台获取用户信息等)的影响,提供两个测试接口,直接传入用户身份信息,比较前后两个方案在不同数量下的性能。

测试环境:本地,链接 dev 数据库

测试工具:Jmeter

测试参数:

1、分页:每页10条数据

2、用户可见范围,相同的用户身份数据

测试1:100条数据

之前的方案

优化后的方案

对比:

数据量100时,之前额方案和优化方案有效请求 7次

  • 之前的方案,90%、95%、99% 指标分别为 120ms、214ms、214ms
  • 优化的方案,90%、95%、99% 指标分别为 112ms、168ms、168ms

接口耗时减少40ms左右。

测试2:1000条数据

之前的方案

优化后方案

对比:

数据量1000时,之前的方案和优化方案有效请求数在 67 次

  • 之前的方案,90%、95%、99% 指标在 362 ms、384 ms、661 ms,与 100条数据相比,耗时变成了3倍
  • 优化的方案,90%、95%、99% 指标在 169 ms、190ms、475 ms,与100条数据相比,90%和95%指标增加了50ms 和 30ms

优化后的方案,性能更加稳定,没有像之前的方案那样,耗时翻3倍。

测试3:10000条数据

之前的方案

接口整体耗时

只查询 article_view_scope 耗时

优化后方案

接口整体耗时

只查询 article_view_scope 耗时

对比:

1、数据量10000时,之前方案和优化方案有效请求数在 616 次

  • 之前的方案,90%、95%、99% 指标在 2481 ms、2567 ms、3113 ms,与 100条数据相比,耗时变成了7倍
  • 优化的方案,90%、95%、99% 指标在 817 ms、848 ms、1494 ms,与1000条数据相比,耗时变成了3倍

2、与数据量1000时相比,优化后方案接口的耗时主要是查询 article_view_scope 的耗时增加,有时不会用到索引,而且查出来的数据量要大于之前的方案,传输也比较耗时

小结

经过上述分析和测试,有以下结论:

优化后的方案,与之前的方案相比,有以下优点:

  • 相同数据量下,比之前的方案性能更好
  • 随着数据量增加,性能更加稳定,不会像之前的方案那样性能迅速下降

缺点:

  • 虽然接口整体性能提升,但是随着数据量增加,查询可见范围时性能会下降

建议直接上 ES,别省钱了。

相关推荐
怀旧666几秒前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
阿华的代码王国21 分钟前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话
德育处主任Pro1 小时前
『Django』APIView基于类的用法
后端·python·django
哎呦没3 小时前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_857600953 小时前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
NiNg_1_2347 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk9 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*9 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue9 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man9 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang