Java Spring 控制器能够同时管理多个 POST 查询,并根据正确的数据库状态进行处理

Java Spring controller able to manage multiple POST queries at the same time and process according to the correct database state

提问人:NicoESIEA 提问时间:11/10/2023 最后编辑:NicoESIEA 更新时间:11/10/2023 访问量:47

问:

我的例子很简单。
我创建了一个端点来从我的接口(或客户端或其他任何内容)获取 JSON 数据。在此示例中,我将收到一个包含一些颜色的 Car json。如果它是已知颜色,我们必须使用这种颜色,否则必须创建新颜色。 下面可以是 POST 端点控制器:

    @RequestMapping(method = RequestMethod.POST, consumes = { "application/json" })
    public Car digest(@RequestBody CarFormat body){
        return CarsService.digest(body.document, body.car);
    }

在 CarsService 摘要方法中,我必须验证每个属性并创建颜色(如果数据库中尚不存在颜色)。

    private Car findColorOrCreate(CarFormat car) {
        String colorReference = car.color.value;
        Assert.notNull(colorReference, String.format("Color reference not found from: %s.", car));
        if (colorService.existsByReference(colorReference)) return colorService.findByReference(colorReference);
        return colorService.create(extractColor(car, colorReference));
    }

正如我们所看到的,服务代码很简单,它检查颜色是否已经存在:

  • 如果是,则返回颜色(由 colorService 中的 findByReference 返回)
  • 否则,它会根据 extractColor 方法创建新颜色)

但是,我创建了一个单元测试(集成)来验证在当时非常接近发送两个查询时的行为:

 @Test
    public void should_createOneColorRecord_whenSimultaneousCalls_Endpoint() throws InterruptedException {
        String sameColorReference = "red";
        String json1 = "...car_id: 1, color:"+sameColorReference+"...";
        String json1 = "...car_id: 2, color:"+sameColorReference+"...";
        String json1 = "...car_id: 3, color:"+sameColorReference+"...";

        String mimeType = "application/json";

        CountDownLatch latch = new CountDownLatch(3);

        executeAsyncRequest("/api/", mimeType, json1, latch);
        executeAsyncRequest("/api/", mimeType, json2, latch);
        executeAsyncRequest("/api/", mimeType, json3, latch);

        latch.await();

        assertThat(entityManager.createQuery("SELECT count(c) FROM Color c WHERE c.reference like '"+sameColorReference+"'").getSingleResult(), equalTo(Long.valueOf(1)));
    }

(当然,在我的例子中,引用的是颜色名称,新颜色已经不存在,实际上数据库中根本没有颜色)

这是 executeAsyncRequest 的代码,我使用每个参数调用端点,但使用不同的 Thread 来模拟真实行为:

    private void executeAsyncRequest(String endpoint, String mimeType, String json, CountDownLatch latch) {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(endpoint)
                .contentType(mimeType)
                .content(json);

        new Thread(() -> {
            try {
                this.mockMvc.perform(builder)
                        .andExpect(MockMvcResultMatchers.status().isOk())
                        .andDo(MockMvcResultHandlers.print());
                latch.countDown();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).start();
    }

最后,当我运行此测试时,预期结果必须是创建了 1 种颜色,但创建了 3 种颜色。 我尝试在控制器、服务和接口上添加 Transactional,也是 @Transactional(propagation = Propagation.REQUIRES_NEW)...... 但没有任何效果。

有人可以帮我解决这个问题吗? 非常感谢提前

Java Spring Database spring-Boot 多线程

评论

0赞 Andrew S 11/10/2023
如果只有一个应用程序实例,最简单的解决方案是同步创建新颜色的代码块,但在该同步代码块中再次检查颜色是否已存在(双重检查锁定)。如果应用程序有多个实例,则需要使用某种时间类型的共享外部锁定机制。
0赞 NicoESIEA 11/10/2023
谢谢@andrew,你能给我一个同步的例子吗?

答:

1赞 Andrew S 11/10/2023 #1

从评论中跟进 - 如果只有一个应用程序实例,那么一个简单的解决方案是使用双重锁定检查。例如:

class ColorService {
   private static final Object lock = new Object();

    public boolean existsByReference(ColorReference r) {
        return ...
    }

    public Car create(Color color) {
        // this code is synchronized across ColorService instances, and only a single Thread can access at a time
       synchronized(lock) { 

            // double lock check - other Thread waiting to also create the new color will return since the new color was already created in another thread
            if (existsByReference(color.getColorReference())) {
                return;
            }

            // safe to create the new color
            return ... // insert into DB
       }
   }
}

评论

0赞 NicoESIEA 11/10/2023
这是我的错误,关键字同步必须紧跟在公众之后,而不是正文里面 ^^谢谢
0赞 Gimby 11/10/2023
请注意:应用程序的单个实例并不一定意味着存在服务类的单个实例。锁定静态最终变量将更加万无一失。
0赞 Andrew S 11/10/2023
根据@Gimby的评论进行更新。