fix(firstleg): avoid shared lock upgrades when initializing customs

This commit is contained in:
2026-10-05 17:03:01 +08:00
parent 261c720c77
commit 4bde1a8352
3 changed files with 13 additions and 4 deletions
@@ -88,9 +88,10 @@ public interface ContainerCustomsMapper {
Long completedPlanForOrder(@Param("tenantId") long tenantId, @Param("orderId") long orderId);
@Insert("""
INSERT IGNORE INTO fl_customs_container
INSERT INTO fl_customs_container
(id,loading_plan_id,container_booking_id,status,created_at,updated_at,version)
VALUES (#{id},#{planId},#{containerBookingId},#{status},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0)
ON DUPLICATE KEY UPDATE id=id
""")
int insertContainer(@Param("id") long id,@Param("planId") long planId,
@Param("containerBookingId") long containerBookingId,@Param("status") String status);
@@ -563,9 +563,10 @@ public class ContainerCustomsService {
if (!"COMPLETED".equals(plan.loadingPlanStatus())) throw conflict("装柜完成后才能处理报关资料");
boolean refund = rows.stream().anyMatch(row -> "TAX_REFUND".equals(row.taxMode()));
mapper.insertContainer(IdWorker.getId(), planId, plan.containerBookingId(), refund ? WAITING : READY);
// A concurrent INSERT IGNORE may wait for another request's insert to commit. The
// transaction's earlier plan reads can retain an RR snapshot that cannot see that row.
// Read the winning aggregate under its row lock, rather than reusing a snapshot read.
// The no-op upsert already takes an exclusive lock on a duplicate, avoiding the
// shared-to-exclusive lock upgrade of INSERT IGNORE followed by FOR UPDATE.
// Earlier plan reads may retain an RR snapshot predating the winning insert, so
// retrieve the real aggregate with a current read, not a snapshot read.
ContainerRow container = mapper.lockContainer(planId);
if (container == null) throw conflict("整柜报关记录初始化失败");
return refresh(planId, container);
@@ -11,6 +11,13 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ContainerCustomsMapperContractTest {
@Test void duplicateInitializationTakesUpdateLockWithoutChangingExistingFacts() throws Exception {
String sql = String.join(" ", ContainerCustomsMapper.class.getMethod("insertContainer",
long.class,long.class,long.class,String.class).getAnnotation(org.apache.ibatis.annotations.Insert.class).value());
assertThat(sql).contains("ON DUPLICATE KEY UPDATE id=id").doesNotContain("INSERT IGNORE");
assertThat(sql.substring(sql.indexOf("ON DUPLICATE KEY UPDATE"))).isEqualTo("ON DUPLICATE KEY UPDATE id=id\n");
}
@Test void initializationFlushesSessionCacheBeforeCurrentAggregateRead() {
var configuration = new org.apache.ibatis.session.Configuration();
configuration.addMapper(ContainerCustomsMapper.class);