Используйте SpringBoot для отправки асинхронных событий, чтобы решить проблему тайм-аута вызова интерфейсного интерфейса.

фон

Система для внутреннего пользования, требующая реализации функций: работа в один клик в фоне от администратора, пакетное обновление и экспорт информации о пользователях и паролях всех обычных пользователей в виде табличного файла время от времени (при проведении мероприятия) Excel.

Цель состоит в том, чтобы предотвратить долгое время, если пароль остается неизменным, его легко мошеннически использовать другими, поэтому необходимо повторно генерировать пароль перед началом каждого нерегулярного действия.

Пароль хранится в зашифрованном виде в базе данных, а алгоритм шифрования — BCrypt, а шифрование реализовано в SpringBootс помощью BCryptPasswordEncoderкласса.

В реальной сцене операция выглядит следующим образом:

  1. Разместите кнопку на главной странице после того, как пользователь нажмет;
  2. Серверный интерфейс сначала запрашивает всех пользователей из базы данных;
  3. Исключить пользователей-администраторов;
  4. Зацикливайте всех обычных пользователей, генерируйте пароли, соответствующие требованиям, выполняйте операции шифрования и выполняйте операции по обновлению таблиц данных;
  5. Создайте Excel и верните.

В начале в тестовой среде было всего десяток пользователей, и в этом процессе все было нормально. После импорта фактического производства более 500 пользовательских данных, поскольку период ожидания, установленный внешним запросом, составляет 10 секунд, время ожидания процесса экспорта файла Excel с информацией о пользователе и пароле истекло, что привело к отключению, то есть этот интерфейс превысит 10 с, когда количество пользователей немного велико. .

Тогда для анализа вышеописанного процесса причинами низкой эффективности могут быть:

  1. Для обновления информации о паролях от пользователя к пользователю в цикле требуется частая запись в базу данных.
  2. Процесс случайной генерации 8-значного пароля может занять много времени: от 8 до 20 цифр, включая прописные и строчные буквы, цифры и комбинации специальных символов _@#$%&*.
  3. Шифрование паролей пользователей может занять много времени: BCryptPasswordEncoder.
  @PostMapping("/updatePasswordAndDownload")
    public void updatePasswordAndDownload(HttpServletResponse response,SysUser user){
    
    

        List<SysUser> list = userService.selectUserList(user);
        List<SysUserExport> userExports = new ArrayList<>();
        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 = new SysUserExport();
                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());
        //导出Excel
        ExcelUtil<SysUserExport> util = new ExcelUtil<SysUserExport>(SysUserExport.class);
        util.exportExcel(response, userExportsSorts, "用户数据", "账号密码");
    }

Целевое решение

Причина 1: Чтобы обновлять информацию о паролях по одному в цикле, базу данных необходимо часто записывать.

В ответ на эту проблему, можем ли мы не использовать базу данных каждый раз в цикле только для обновления поля пароля пользователя; вместо этого использовать один SQLдля непосредственного обновления всех паролей пользователей в пакетах?

Мы знаем, что в SQL, Case Whenэто требование может быть реализовано через утверждение. Итак, теперь с помощью MyBatisэтого мы можем добиться пакетного обновления разных паролей пользователей:

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

Затем вы можете forвызвать указанный выше интерфейс для пакетного обновления паролей пользователей вне цикла; однако, даже если вы вызовете указанный выше метод вне цикла, время ожидания интерфейса все равно истечет. . Так что проблема не в этом круговом обновлении.

Причина 2: Процесс случайной генерации 8-значного пароля может занять много времени: от 8 до 20 цифр, включая прописные и строчные буквы, цифры и комбинации специальных символов _@#$%&*.

Так как случайная генерация паролей является инструментальным методом, я непосредственно протестировал этот метод в одиночку.После генерации 500 паролей в пакетах я обнаружил, что затраты времени были очень короткими и незначительными.Поэтому проблема здесь не в этом. .

Причина 3: Шифрование паролей пользователей может занять много времени: BCryptPasswordEncoder.

Точно так же метод шифрования паролей пользователей также является инструментальным методом.После шифрования 500 паролей в пакетах обнаруживается, что время определенно превышает 10 с, что прямо неприемлемо. BCryptПроцесс шифрования действительно медленный, но на самом деле он, как правило, предназначен для шифрования пароля пользователя, что не похоже на пакетную операцию, с которой мы сталкиваемся сейчас. Наконец-то проблема найдена~~

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

По Bcryptвопросу медленной скорости шифрования я прочитал SegmentFaultподробную статью о том, слаба ли медленная скорость шифрования Bcrypt? .

асинхронное решение

Обязательно нужно зашифровать пароль пользователя, но что делать, если время ожидания интерфейса истекло? Далее давайте пригласим главного героя этой статьи на его дебют, асинхронные события.

Чтобы сократить время отклика интерфейса, после того, как пользователь нажмет кнопку для экспорта и обновления пароля пользователя, сначала установите исходный пароль и запишите его в экспортированный файл для ответа внешнему пользователю, затем используйте встроенный для отправки асинхронных событий и выполните трудоемкие операции шифрования пароля и обновления таблицы данных в методе асинхронного мониторинга событий (здесь также учитывается посылка: после того, как пользователь экспортирует имя пользователя и пароль, эти пользователи не сразу войдут в систему с сгенерированным новым паролем, поэтому требуется 1-2 минуты, чтобы обновлять таблицу данных асинхронно, что не должно быть большой Excelпроблемой Spring) ApplicationEventPublisher.

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @PostMapping("/updatePasswordAndDownload")
    public void updatePasswordAndDownload(HttpServletResponse response,SysUser user){
    
    

        List<SysUser> list = userService.selectUserList(user);
        List<SysUserExport> userExports = new ArrayList<>();

        //去除超级管理员和系统管理员、审计管理员
        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 = new SysUserExport();
            sysUserExport.setPassword(password);
            sysUserExport.setUserId(newuser.getUserId());
            sysUserExport.setUserName(newuser.getUserName());
            sysUserExport.setDept(newuser.getDept().getDeptName());
            userExports.add(sysUserExport);
        }

        // 发送事件
        PasswordEvent passwordEvent = new PasswordEvent(this, collected);
        applicationEventPublisher.publishEvent(passwordEvent);

        //根据部门排序
        List<SysUserExport> userExportsSorts = userExports.stream().sorted(Comparator.comparing(SysUserExport::getDept).reversed()).collect(Collectors.toList());
        //导出Excel
        ExcelUtil<SysUserExport> util = new ExcelUtil<SysUserExport>(SysUserExport.class);
        util.exportExcel(response, userExportsSorts, "用户数据","投票系统账号密码");
    }

На стороне прослушивания событий отслеживайте асинхронные события с помощью @EnableAsyncаннотаций и затем обрабатывайте трудоемкие операции в прослушивателе событий.@EventListener

Поскольку в действительности конечных пользователей всего несколько сотен, метод обновления паролей один за другим в цикле можно использовать напрямую.

@Component
@EnableAsync
public class PasswordListener {
    
    
    @Autowired
    private ISysUserService userService;

    @EventListener
    @Async
    public void passwordEventHandler(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
@EnableAsync
public class PasswordListener {
    
    

    @Autowired
    private ISysUserService userService;

    @EventListener
    @Async
    public void passwordEventHandler(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асинхронное решение используется для решения проблемы тайм-аута генерации паролей, шифрования и экспорта из-за увеличения числа пользователей.

Ссылка

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


Если у вас есть какие-либо вопросы или обнаружены какие-либо ошибки, пожалуйста, свяжитесь со мной.

Ваши комментарии и предложения приветствуются!

おすすめ

転載: blog.csdn.net/u013810234/article/details/130903661