侧边栏壁纸
博主头像
ldwcool's Blog博主等级

行动起来,活在当下

  • 累计撰写 24 篇文章
  • 累计创建 10 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Spring 事务传播行为

ldwcool
2023-10-08 / 0 评论 / 0 点赞 / 41 阅读 / 5318 字

Preface

前两天在运行代码插入数据的时候总是报索引重复,数据死活插入不了。

情景再现

环境:Springboot,JPA。

数据库中有一张表,姑且称为table_a,结构如下:

@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Entity
@Table(name = TableA.TABLE_NAME, indexes = {
        @Index(name = "idx_spuId", columnList = "spuId", unique = true)
})
public class TableA {

    public static final String TABLE_NAME = "table_a";

    /**
     * 主键id
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = TABLE_NAME + "_generator")
    private Integer id;

    /**
     * 商品spu_id
     */
    @NotNull
    @ColumnDefault("0")
    private Long spuId = 0L;

    /**
     * 商品名称
     */
    private String goodsName;

    /**
     * 商品信息更新次数
     */
    @NotNull
    @ColumnDefault("0")
    private Integer updateNum = 0;

}

对应dao内容如下:

public interface ITableADao extends JpaRepository<TableA, Integer> {

    /**
     * 根据 spuId 查询
     * @param spuId spuId
     * @return -
     */
    TableA findFirstBySpuId(@NotNull Long spuId(@NotNull Long spuId);

}

有两个 Service 实现,分别称为ServiceAImplServiceBImpl吧。

ServiceAImpl中的add方法首先判断要插入的商品是否已在数据库中了,存在则更新数据,不存在则插入数据。

@Service
@RequiredArgsConstructor
public class ServiceAImpl implements ServiceA {

    private final ITableADao tableADao;

    /**
     * 插入商品信息
     *
     * @param form form
     * @return -
     */
    @Override
    @Transactional(propagation= Propagation.REQUIRES_NEW)
    public TableA add(TableA form) {

        TableA tableA = tableADao.findFirstBySpuId(form.getSpuId());

        if (tableA == null) {
            // 插入数据
            tableA = new TableA();
        }

        // 更新数据
        // 模拟调用第三方接口查询数据
        TableA result = Api.searchBySpuId(spuId);
        BeanUtils.copyProperties(result, tableA);
        tableADao.save(tableA);

        return tableA;
    }
}

ServiceBImpl中调用ServiceAImpladd方法。

@Service
@RequiredArgsConstructor
public class ServiceBImpl implements IServiceB{

    private final IServiceA serviceA;
    private final ITableADao tableADao;

    /**
     * 处理商品信息
     *
     * @param spuId spuId
     * @return -
     */
    @Override
    @Transactional
    public boolean handleGoods(Long spuId) {

        // 前面的一些代码逻辑,省略...

        deal(spuId);

        // 后面的一些代码逻辑,省略...

        return true;
    }

    /**
     * 处理商品信息
     *
     * @param spuId spuId
     */
    private void deal(Long spuId) {

        TableA tableA = serviceA.add(spuId);

        // 设置商品的更新次数
        tableA.setUpdateNum(tableA.getUpdateNum());
        tableADao.save(tableA);
    }

}

发生的异常情况是在调用handleGoods方法传入数据库中不存在的spuId时,在save时会报错,提示重复的spuId,无法插入数据。

// 设置商品的更新次数
tableA.setUpdateNum(tableA.getUpdateNum());
tableADao.save(tableA);

这就很奇怪了,明明插入的spuId是数据库中不存在的,为什么会报spuId重复呢?

经过一番简单的思考,发现是add方法上的

@Transactional(propagation= Propagation.REQUIRES_NEW)

在作祟。add方法中开启了一个新的事务,在插入新的数据时,就会导致在别的事务中查不到新插入的数据,因为此时数据还没有真正插入到数据库中。所以

// 设置商品的更新次数
tableA.setUpdateNum(tableA.getUpdateNum());
tableADao.save(tableA);

save方法由于事务不同,查不到刚刚在ServiceAImpladd方法添加的商品信息,也会执行insert语句。最终在提交事务时add方法中的savedeal方法中的save都会执行insert语句,就导致报spuId重复的错误。

解决方法就是将add方法上的

@Transactional(propagation= Propagation.REQUIRES_NEW)

改为

@Transactional(propagation= Propagation.REQUIRED)

这也是 Spring 默认的事务传播行为,表示没有就创建事务,有就加入事务

至于之前为什么这么写,那是之前设计的问题了😂。

Spring 事务传播行为

事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。 例如:methodA 事务方法调用 methodB 事务方法时,methodB 是继续在调用者 methodA 的事务中运行呢,还是为自己开启一个新事务运行,这就是由 methodB 的事务传播行为决定的。

Spring 目前共有 7 种事务传播行为。

事务传播行为类型说明
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

参考资料

0

评论区