Spring Boot 并发/多个请求导致有状态数据库表上的数据交错/损坏

问题描述 投票:0回答:1

总结

在 Spring Boot 后端上工作,该后端有一个连接到前端按钮的端点。该端点对输入进行一些重构,然后调用两个单独的 JPA 存储库方法。第一个更新表中的记录,第二个插入一条新记录。有时似乎有多个请求到达该端点,并且这些方法没有被正确处理。这可能是由于用户向按钮发送垃圾邮件,或多个用户发出相同的请求。为了进行调试,我创建了生成唯一 UUID 的日志,并在每个线程进入端点、调用服务以及随后调用存储库方法时跟踪每个线程。据我所知,更新和插入并不是作为单个操作完成的,并且一些交错导致更多更新在其他更新之前运行,而不是它们的“匹配”插入。

限制

  • 无法访问前端代码
  • 无法创建存储过程(DBA 不允许)

数据库表

这张桌子是已经存在很久的桌子了,所以暂时无法修改它的设计。

它的设计方式是它有一个 IS_LATEST 列,基本上跟踪项目的最新状态,但是当应用程序需要更新该项目的状态时,需要发生两件事:

  1. 更新现有的 IS_LATEST(当前)记录,使其 IS_LATEST 值为 FALSE,表示该项目的过去状态。
  2. 插入包含所有新属性值、最新属性值且 IS_LATEST 为 TRUE 的新记录

代码

控制器

    @Override
    @PostMapping("/updateMyDataTrackerLog/{dataLevel}/{targetMonth}/{screen}")
    public ResponseEntity<ResponseVO> updateCompletionStatus(@RequestBody final List<MyDataTrackerLogVo> request,
                                                             final HttpServletRequest httpServletRequest,
                                                             @PathVariable(value = "dataLevel") final String dataLevel,
                                                             @PathVariable(value = "targetMonth") final String targetMonth,
                                                             @PathVariable(value = "screen") final Long screen) {
        try {
            final String userName = helper.getUserID(httpServletRequest);
            service.updateCompletionStatus(dataLevel, targetMonth, screen, request, userName);
            return new ResponseEntity<>(new ResponseVO<>("Success"), HttpStatus.CREATED);
        } catch (Exception e) {
            return new ResponseEntity<>(new ResponseVO("Error"), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

服务

    @Override
    public void updateCompletionStatus(final String dataLevel, final String targetMonth,
                                       final Long screen, final List<MyDataTrackerLogVo> request, String username) {
        String uuid = String.valueOf(UUID.randomUUID()); // used for logging, removed logs for brevity
        Map<String, Object> preparedInputsForUpdate = prepareMyDataTrackerLogForUpdate(dataLevel, targetMonth,
                screen, request, username, uuid);
        updateAndInsertMyDataTrackerLogs(preparedInputsForUpdate, uuid, username);
    }

    @Override
    public Map<String, Object> prepareMyDataTrackerLogForUpdate(final String dataLevel, final String targetMonth,
                                                                 final Long screen,
                                                                 final List<MyDataTrackerLogVo> request,
                                                                 final String username, String uuid) {
        List<MyDataTrackerLog> batchedMyDataTrackerLogs = new ArrayList<>();
        final Map<String, Object> inputs = new HashMap<>();
        if (request != null && !request.isEmpty()) {
            request.forEach(log -> {
                final MyDataTrackerLog myDataTrackerLog = DataSetTrackerMapper.INSTANCE.toDataSetTrackerToDomain(log);
                myDataTrackerLog.setLatest(true);
                // sets a bunch of properties
                batchedMyDataTrackerLogs.add(myDataTrackerLog);
            });

            List<String> possibleNestedCollectionOfStrings = request.stream()
                    .map(MyDataTrackerLogVo::getPossibleNestedCollectionOfStrings)
                    .collect(toList());
            inputs.put("possibleNestedCollectionOfStrings", possibleNestedCollectionOfStrings);
            inputs.put("myDataTrackerLogsBatch", batchedMyDataTrackerLogs);
        }
        return inputs;
    }

    protected void updateAndInsertMyDataTrackerLogs(Map<String, Object> inputs, String uuid, String username) {
        List<MyDataTrackerLog> batch = (List<MyDataTrackerLog>) inputs.get("myDataTrackerLogsBatch");
        List<String> possibleNestedCollectionOfStrings = (List<String>) inputs.get("possibleNestedCollectionOfStrings");
        MyDataTrackerLog logMetaData = batch.get(0);
         myDataTrackerLogRepository.updateIsLatestBatchedByPossibleNestedCollectionOfStrings(
                false,
                logMetaData.getBusinessYearMonth(),
                logMetaData.getLevel2Bu(),
                logMetaData.getScreenId(),
                possibleNestedCollectionOfStrings
        );
        myDataTrackerLogRepository.saveAll(batch);
    }

Repo 更新方法

    @Modifying
    @Transactional
    @Query("Update MyDataTrackerLog d SET "
            + "d.isLatest = :isLatest "
            + "where d.targetMonth = :targetMonth "
            + "AND d.level = :level AND d.screen = :screen "
            + "AND d.possibleNestedCollectionOfStrings in (:possibleNestedCollectionOfStrings) AND d.isLatest = true")
    void updateIsLatestBatchedByPossibleNestedCollectionOfStrings(
            final @Param("isLatest") Boolean isLatest, final @Param("targetMonth") Date targetMonth,
            final @Param("level") String level, final @Param("screen") Long screen,
            final @Param("possibleNestedCollectionOfStrings") List<String> possibleNestedCollectionOfStrings
    );

注释

  • 为了简洁起见,我不得不修改一些代码,因为还有很多 BL 正在进行,但与这个问题无关。
  • 请求的第一级属性都相同,但有一个嵌套属性可以包含值列表,例如:

请求一个

  • 道具1
  • prop2 列表 { 'justOneThing' }

请求两个

  • 道具1
  • prop2 列表 { '1', '2', '3' }

第二个属性正在使用

IN
SQL 语法进行批处理和调用,以一次性处理所有属性,而不是循环(这是之前尝试解决此并发问题的建议)

结果

因此,理想情况下,更新将清除所有匹配的项目记录并将其 IS_LATEST 设置为 false,然后立即进行插入,这将为数据库提供项目记录的最新表示。不幸的是,我看到这样的处理:

来自同一用户的相同请求(按钮垃圾邮件场景)::Interleaving

主题 1 主题 2
开始更新
开始更新
完成更新
开始插入
完成更新
开始插入
完成插入
完成插入

我认为正在发生的是 T2 完成更新没有捕获第一个插入,导致有两个“当前”项目记录。

结论/问题

我怀疑这里的解决方案是进行更新并插入单个原子化操作,而我能想到的唯一方法是锁定表,但我没有成功地尝试使用注释手动实现此操作,然后通过冬眠。我一定做错了什么。有没有其他方法可以从服务器端解决这个并发问题?

sql-server spring-boot concurrency
1个回答
0
投票

您想在一个原子事务中执行此操作,这是正确的。并发很容易出错,但谨慎起见,下面的方法会犯错误。

下面是 Transact-SQL 语句,但您可以使用 JPA 注释、jdbc 调用等执行其中一些步骤。

主题 1

  1. 设置可串行化的事务隔离级别
  2. 开始传输
  3. 从 T1 中选择 .. WHERE IS_LATEST = 1
  4. 更新 T1 集 IS_LATEST = 0
  5. 插入....
  6. 提交交易

SERIALIZABLE 隔离模式导致第一个 SELECT 充当互斥锁,阻塞其他行,直到事务提交(或回滚)。

您可以使用查询提示(SELECT 上的 UPLOCK、HOLDLOCK)实现相同的效果。使用查询提示的好处是您无需事后重置隔离模式。 (JDBC 连接池并不总是在借用之间将事物重置为干净的状态,并且在任何地方使用 SERIALIZABLE 可能不是您想要的)。

可串行化隔离模式

UPDLOCK,HOLDLOCK相关问题

© www.soinside.com 2019 - 2024. All rights reserved.