0


使用SpringBoot发送异步事件的方式解决前端接口调用超时问题

背景

一个内部使用的系统,要求实现功能:管理员后台一键操作,不定期(举办活动时)批量更新并导出所有普通用户的用户与密码信息为

Excel

表格文件。

目的是防止时间长了,如果密码不变的话,容易被别人冒用,所以每次不定期的活动开始前,要求重新生成密码。

密码在数据库中是密文存储,加密算法为

BCrypt

,在

SpringBoot

中借助

BCryptPasswordEncoder

类实现加密。

实际场景中操作如下:

  1. 前端页面放一个按钮,用户点击后;
  2. 后端接口先从数据库中查询所有用户;
  3. 排除掉管理员用户;
  4. 循环所有普通用户,生成满足要求的密码,执行加密操作,执行更新数据表操作;
  5. 生成Excel并返回。

一开始在测试环境下,就十来个用户,这个过程一切正常。导入了实际生产的500+用户数据后,由于前端请求设置的超时时间为10秒,导出用户与密码信息的Excel文件过程超时导致断开连接了,即:这个接口在用户数量稍微多的时候就超过10s了。。

那么来分析上面的过程,导致效率低的原因可能有:

  1. 在循环中逐个用户去更新密码信息,需要频繁写数据库。
  2. 随机生成8位密码过程可能比较耗时:8至20位,包含大、小写字母、数字、特殊字符_@#$%&*组合。
  3. 对用户密码加密可能比较耗时:BCryptPasswordEncoder。
@PostMapping("/updatePasswordAndDownload")publicvoidupdatePasswordAndDownload(HttpServletResponse response,SysUser user){List<SysUser> list = userService.selectUserList(user);List<SysUserExport> userExports =newArrayList<>();String password =null;for(SysUser newuser:list){//去除超级管理员和系统管理员、审计管理员if(!newuser.isAdmin()&&!newuser.isSystem()&&!newuser.isAudit()){
                userService.checkUserAllowed(newuser);
                userService.checkUserDataScope(newuser.getUserId());//设置8位随机密码并重置存储密码
                password =PasswordUtil2.getPsw(UserConstants.PASSWORD_MIN_LENGTH);// 导致接口超时,可能的原因2
                newuser.setPassword(SecurityUtils.encryptPassword(password));// 导致接口超时,可能的原因3
                newuser.setUpdateBy(getUsername());
                userService.resetPwd(newuser);// 导致接口超时,可能的原因1//更新导出用户实体SysUserExport sysUserExport =newSysUserExport();
                sysUserExport.setPassword(password);
                sysUserExport.setUserId(newuser.getUserId());
                sysUserExport.setUserName(newuser.getUserName());
                sysUserExport.setDept(newuser.getDept().getDeptName());
                userExports.add(sysUserExport);}}//根据部门排序List<SysUserExport> userExportsSorts = userExports.stream().sorted(Comparator.comparing(SysUserExport::getDept).reversed()).collect(Collectors.toList());//导出ExcelExcelUtil<SysUserExport> util =newExcelUtil<SysUserExport>(SysUserExport.class);
        util.exportExcel(response, userExportsSorts,"用户数据","账号密码");}

针对性解决

原因1:在循环中逐个用户去更新密码信息,需要频繁写数据库。

针对这个问题,我们能不能不要在循环中每次都去操作数据库,只为更新用户的密码字段;而是用一条

SQL

直接批量更新所有的用户密码呢?

我们知道在

SQL

中,可以通过

Case When

语句来实现这一需求。那么现在,借助

MyBatis

,我们可以通过以下方法实现对不同用户密码的批量更新:

<updateid="updatePasswordBatch"parameterType="java.util.List">
        update sys_user
        <trimprefix="set"suffixOverrides=","><trimprefix="password=case"suffix="end,"><foreachcollection="list"item="item"index="index"><iftest="item.password!=null">
                        when user_id=#{item.userId} then #{item.password}
                    </if></foreach></trim></trim>
        where user_id in
        <foreachcollection="list"index="index"item="item"separator=","open="("close=")">
            #{item.userId, jdbcType=BIGINT}
        </foreach></update>

之后便可以在

for

循环外调用上面这个批量更新用户密码的接口;然而,即使是在循环外调用上面的方法,接口依然超时。。所以,问题不在这个循环更新上。

原因2:随机生成8位密码过程可能比较耗时:8至20位,包含大、小写字母、数字、特殊字符_@#$%&*组合。

由于随机生成密码是一个工具方法,我直接单独测试该方法,批量生成500个密码后发现耗时非常短,可以忽略不计,因此,问题也不在这里。。

原因3:对用户密码加密可能比较耗时:BCryptPasswordEncoder。

同样,对用户密码加密的方法也是一个工具方法,批量加密500个密码后发现,耗时绝对超过10s,直接不可接受。

BCrypt

加密过程确实慢,但是实际一般都是对一个用户的密码进行加密,不会像我们现在遇到的批量操作。终于,问题找见啦~~

/**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */publicstaticStringencryptPassword(String password){BCryptPasswordEncoder passwordEncoder =newBCryptPasswordEncoder();return passwordEncoder.encode(password);}

关于

Bcrypt

加密速度慢的问题,我看了

SegmentFault

的一个帖子上有详细说明:Bcrypt加密速度慢是否是鸡肋?。

异步解决方案

用户密码进行加密肯定是要做的,可是导致接口超时了怎么办?接下来,有请本文的主角闪亮登场,异步事件。

为减少接口响应时间,在用户点击导出并更新用户密码的按钮后,先设置密码原文,写入到导出的

Excel

文件中响应给前端用户;然后使用

Spring

自带的

ApplicationEventPublisher

发送异步事件,在异步事件监听方法中进行耗时的密码加密与数据表更新操作(这里还考虑到一个前提:用户导出用户名与密码后,这些用户并不会立即使用生成的新密码进行登录,因此异步更新数据表需要花费1-2分钟应该没有大的问题)。

@AutowiredprivateApplicationEventPublisher applicationEventPublisher;@PostMapping("/updatePasswordAndDownload")publicvoidupdatePasswordAndDownload(HttpServletResponse response,SysUser user){List<SysUser> list = userService.selectUserList(user);List<SysUserExport> userExports =newArrayList<>();//去除超级管理员和系统管理员、审计管理员List<SysUser> collected = list.stream().filter(x ->!x.isAdmin()&&!x.isSystem()&&!x.isAudit()).collect(Collectors.toList());for(SysUser newuser : collected){
            userService.checkUserAllowed(newuser);
            userService.checkUserDataScope(newuser.getUserId());//设置8位随机密码并重置存储密码String password =PasswordUtil2.getPsw(UserConstants.PASSWORD_MIN_LENGTH);// 为减少接口响应时间,这里先设置密码原文,在异步事件中进行耗时的密码加密与更新操作
            newuser.setPassword(password);

            newuser.setUpdateBy(getUsername());//                userService.resetPwd(newuser);//更新导出用户实体SysUserExport sysUserExport =newSysUserExport();
            sysUserExport.setPassword(password);
            sysUserExport.setUserId(newuser.getUserId());
            sysUserExport.setUserName(newuser.getUserName());
            sysUserExport.setDept(newuser.getDept().getDeptName());
            userExports.add(sysUserExport);}// 发送事件PasswordEvent passwordEvent =newPasswordEvent(this, collected);
        applicationEventPublisher.publishEvent(passwordEvent);//根据部门排序List<SysUserExport> userExportsSorts = userExports.stream().sorted(Comparator.comparing(SysUserExport::getDept).reversed()).collect(Collectors.toList());//导出ExcelExcelUtil<SysUserExport> util =newExcelUtil<SysUserExport>(SysUserExport.class);
        util.exportExcel(response, userExportsSorts,"用户数据","投票系统账号密码");}

在事件监听端,通过

@EnableAsync

@EventListener

注解实现对异步事件的监听,然后在事件监听器中处理耗时的操作。

因为实际中的最终用户也就几百个,可直接采用循环逐个更新密码的方式。

@Component@EnableAsyncpublicclassPasswordListener{@AutowiredprivateISysUserService userService;@EventListener@AsyncpublicvoidpasswordEventHandler(PasswordEvent passwordEvent){// 从事件中获取事件源List<SysUser> users = passwordEvent.getMsg();System.out.println("监听到PasswordEvent事件");for(SysUser user : users){
            user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
            userService.resetPwd(user);}}}

或者采用

MyBatis

的批量更新密码的方式也可以。

@Component@EnableAsyncpublicclassPasswordListener{@AutowiredprivateISysUserService userService;@EventListener@AsyncpublicvoidpasswordEventHandler(PasswordEvent passwordEvent){// 从事件中获取事件源List<SysUser> users = passwordEvent.getMsg();System.out.println("监听到PasswordEvent事件");for(SysUser user : users){
            user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));//            userService.resetPwd(user);}// 批量更新用户密码
        userService.updatePasswordBatch(users);}}

小总结

以上便是因接口超时问题引发的原因分析和对应的解决方法,最终采用

Spring

自带的

ApplicationEventPublisher

异步方案解决因用户量增大导致生成密码、加密、导出的超时问题。

Reference

https://blog.csdn.net/weixin_44227650/article/details/126408514


If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!


本文转载自: https://blog.csdn.net/u013810234/article/details/130903661
版权归原作者 Heartsuit 所有, 如有侵权,请联系我们删除。

“使用SpringBoot发送异步事件的方式解决前端接口调用超时问题”的评论:

还没有评论