【问题标题】:How to write correct/reliable transactional code with JAX-RS and Spring如何使用 JAX-RS 和 Spring 编写正确/可靠的事务代码
【发布时间】:2016-12-26 22:09:40
【问题描述】:

基本上,在使用 Jax-RS 和 Spring 开发 REST 服务时,我试图了解如何编写正确(或“正确编写”?)事务代码。此外,我们使用 JOOQ 进行数据访问。但这应该不是很相关...
考虑一个简单的模型,我们有一些组织,它们有这些字段:"id", "name", "code"。所有这些都必须是唯一的。还有一个status 字段。
组织可能会在某个时候被删除。但我们不想完全删除数据,因为我们想将其保存用于分析/维护目的。所以我们只需将组织“状态”字段设置为'REMOVED'
因为我们没有从表中删除组织行,所以我们不能简单地将唯一约束放在“名称”列上,因为我们可能会删除组织,然后创建一个具有相同名称的新组织。但是让我们假设代码必须是全局唯一的,所以我们对 code 列有唯一约束。

因此,让我们看看这个简单的示例,它创建了组织,并在此过程中执行了一些检查。

资源:

@Component
@Path("/api/organizations/{organizationId: [0-9]+}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaTypeEx.APPLICATION_JSON_UTF_8)
public class OrganizationResource {
    @Autowired
    private OrganizationService organizationService;

    @Autowired
    private DtoConverter dtoConverter;

    @POST
    public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) {

        if (organizationService.checkOrganizationWithNameExists(request.name())) {
            // this throws special Exception which is intercepted and translated to response with 409 status code
            throw Responses.abortConflict("organization.nameExist", ImmutableMap.of("name", request.name()));
        }

        if (organizationService.checkOrganizationWithCodeExists(request.code())) {
            throw Responses.abortConflict("organization.codeExists", ImmutableMap.of("code", request.code()));
        }

        long organizationId = organizationService.create(person.user().id(), request.name(), request.code());
        return dtoConverter.from(organization.findById(organizationId));
    }
}

DAO 服务如下所示:

@Transactional(DBConstants.SOME_TRANSACTION_MANAGER)
public class OrganizationServiceImpl implements OrganizationService {
    @Autowired
    @Qualifier(DBConstants.SOME_DSL)
    protected DSLContext context;

    @Override
    public long create(long userId, String name, String code) {
        Organization organization = new Organization(null, userId, name, code, OrganizationStatus.ACTIVE);
        OrganizationRecord organizationRecord = JooqUtil.insert(context, organization, ORGANIZATION);
        return organizationRecord.getId();
    }

    @Override
    public boolean checkOrganizationWithNameExists(String name) {
        return checkOrganizationExists(Tables.ORGANIZATION.NAME, name);
    }

    @Override
    public boolean checkOrganizationWithCodeExists(String code) {
        return checkOrganizationExists(Tables.ORGANIZATION.CODE, code);
    }

    private boolean checkOrganizationExists(TableField<OrganizationRecord, String> checkField, String checkValue) {
        return context.selectCount()
                .from(Tables.ORGANIZATION)
                .where(checkField.eq(checkValue))
                .and(Tables.ORGANIZATION.ORGANIZATION_STATUS.ne(OrganizationStatus.REMOVED))
                .fetchOne(DSL.count()) > 0;
    }
}

这带来了一些问题:

  1. 我应该把@Transactional注解放在Resource的createOrganization方法上吗?或者我应该再创建一个与 DAO 对话的服务并将 @Transactional 注释放到它的方法中?还有什么?
  2. 如果两个用户同时发送具有相同"code" 字段的请求会发生什么情况。在提交第一个事务之前,检查已成功通过,因此不会发送 409 响应。比第一个事务将正确提交,但第二个事务将违反数据库约束。这将抛出 SQLException。如何优雅地处理它?我的意思是我仍然想在客户端显示漂亮的错误消息,说该名称已被使用。但我不能真正解析 SQLException 或 smth.. 我可以吗?
  3. 与上一个类似,但这次“名称”不是唯一的。在这种情况下,第二笔交易不会违反任何约束,这会导致两个组织同名,这违反了我们的业务约束。
  4. 我在哪里可以看到/学习教程/代码/等,您会考虑如何编写具有复杂业务逻辑的正确/可靠的 REST+DB 代码的很好的示例。 Github/书籍/博客,随便。我自己试图找到类似的东西,但大多数示例只关注管道 - 将这些库添加到 maven,使用这些注释,最后就是你的简单 CRUD。它们根本不包含任何交易考虑。即

更新: 我知道隔离级别和通常的error/isolation matrix(脏读等)。我遇到的问题是找到一些“生产就绪”的样本来学习。或者是一本关于某个主题的好书。我仍然没有真正了解如何正确处理所有错误..我想我需要重试几次,如果事务失败..而不仅仅是抛出一些通用错误并实现客户端,它可以处理..但是做每当我使用范围查询时,我真的必须使用 SERIALIZABLE 模式吗?因为它会极大地影响性能。但否则我怎么能保证交易会失败..

无论如何,我已经决定,现在我需要更多时间来学习事务和数据库管理,以解决这个问题......

【问题讨论】:

    标签: java spring rest transactions jax-rs


    【解决方案1】:

    关注点分离:

    • Jax-rs 资源(端点)层:只需处理请求、调用服务并将潜在异常包装在适当的响应代码中(只需手动捕获和包装或使用exception mapper)。
    • 服务/业务层:为每个工作单元公开一个事务方法,业务错误必须作为已检查异常处理,操作错误作为未检查异常处理(RuntimeException 的子类)。
    • 数据访问层:只处理数据访问的东西(即获取数据库上下文、执行查询并最终映射结果)。

    我坚持一件事,拥有事务边界的好地方是定义业务方法的地方。事务范围必须是工作的业务单元。

    关于并发问题,有两种方法可以处理这种并发问题:悲观锁定或乐观锁定。

    • 悲观:

      • 锁定
      • 做你的事
      • 更新
      • 解除锁定
    • 乐观:

      • 检查版本
      • 做你的事
      • 如果版本相同则更新,否则失败

    悲观是关于可扩展性和性能的问题,乐观的问题是您有时会通过向最终用户发送操作错误来结束。

    我个人会选择乐观锁定,JOOQ support it

    【讨论】:

    • 对于乐观锁,不清楚应该对哪些记录进行版本化?我想保护 INSERT 操作不创建重复条目。但在那种情况下,我并没有真正改变任何记录,是吗?我想,我可以让它以某种方式工作......就像创建包含所有曾经使用过的名称的表 - 但它看起来很丑......至于悲观锁 - 你的意思是我应该使用SELECT..FOR UPDATE吗?还是只使用REPEATABLE READ 隔离?
    • 我只是不明白,如果它真的能保护我的情况,我插入新记录作为通过选择其他记录做出的决定,即检查名称是否存在,然后插入具有该名称的新记录,否则什么也不做。
    • 对不起,我读得太快了。 REPEATABLE READ 不会阻止插入新数据,您必须锁定整个表,因此将级别设置为可序列化。我想select for update 只锁定选定的语句,所以不合适。您可以在专用表中维护一个 org 表版本条目以实现乐观锁定并在同一事务中插入新的 org 和增量版本,但这不是很优雅
    【解决方案2】:

    根据我的理解,处理数据库级事务的最佳方式必须在 dao 层中有效地使用 Spring 的 Isolation trnsaction。以下是您案例中的示例行业标准代码...

    public interface OrganizationService {
        @Retryable(maxAttempts=3,value=DataAccessResourceFailureException.class,backoff=@Backoff(delay = 1000))
        public boolean checkOrganizationWithNameExists(String name);    
    }
    
    @Repository
    @EnableRetry
    public class OrganizationServiceImpl implements OrganizationService {
        @Transactional(isolation = Isolation.READ_COMMITTED)
        @Override
        public boolean checkOrganizationWithNameExists(String name){
            //your code
            return  true;       
        }
    }
    

    如果我说错了请掐我

    【讨论】:

      【解决方案3】:

      一般来说,不讨论事务性,端点应该只从请求中获取参数并调用服务。它不应该做业务逻辑。

      您的 checkXXX 方法似乎是业务逻辑的一部分,因为它们会引发有关特定于域的冲突的错误。为什么不把它们放到 Service 中变成一种方法,顺便提一下事务呢?

      //service code
      public Organization createOrganization(String userId, String name, String code) {
      
          if (this.checkOrganizationWithNameExists(request.name())) {
              throw ...
          }
      
          if (this.checkOrganizationWithCodeExists(code)) {
              throw ...
          }
      
          long organizationId = this.create(userId, name, code);
          return dao.findById(organizationId);
      }
      

      我认为你的参数是字符串,但它们可以是任何东西。我不确定您是否要在服务层中抛出 Responses.abortConflict,因为它似乎是一个 REST 概念,但您可以根据需要为其定义自己的异常类型。

      端点代码应如下所示,但是,它可能包含额外的 try-catch 块,可将抛出的异常转换为错误响应:

      //endpoint code
      @POST
      public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) {
          String code = request.code();
          String name = request.name();
          String userId = person.user().id();
          return dtoConverter.from(organizationService.createOrganization(userId, name, code));
      }
      

      至于问题 2 和 3,transaction isolation levels 是你的朋友。将隔离级别足够高。我认为“可重复阅读”适合您的情况。您的 checkXXX 方法将检测其他事务是否提交了具有相同名称或代码的实体,并保证在执行“创建”方法时情况保持不变。还有一个关于 Spring 和事务隔离级别的 useful read

      【讨论】:

      • 但它仍然无助于解决数据不正确的问题。意思是问题中的问题 2 和 3...
      • 数据检查在服务中完成,你抛出你自己的异常。您不必解析 SQLException。事务隔离保证一旦您在服务中进行检查,它们在“创建”语句期间仍然有效。即使两个请求同时到达,其中一个也会被延迟,直到另一个被提交(或类似的东西,这取决于级别)。我认为这涵盖了问题 2 和 3。
      • 事情没那么简单... SERIALIZABLE 隔离对于每种情况都过于严格,但即使是 REPEATABLE_READ 也是不够的。我不得不使用 SELECT FOR UPDATE 让它以某种方式工作,但是那样并发会受到影响......顺便说一句,我已经把测试项目放在 github 上,所以任何人都可以使用它......github.com/Fantast/gs-managing-transactions
      • 如果可伸缩性是一个问题,并且您需要非常高的吞吐量来执行此特定操作,那么可以做一些事情。例如缓存名称和代码并在开始事务之前检查缓存。此外,如果可伸缩性确实是一个问题,您最好熟悉从 SQLExceptions 解析违反约束并摆脱显式检查。
      【解决方案4】:

      首先,DAO 层甚至不应该知道它的前端是 REST Web 服务。请务必分开职责。

      将@Transactional 保留在DAO 上。如果您只发出一条语句,那么您需要确定是否可以接受脏读。基本上,找出您的应用程序的最低隔离级别是多少。每个方法都将启动一个新的事务(除非从另一个已经启动过的方法调用),如果抛出任何异常,它将回滚任何调用。您可以在控制器中设置自定义 ExceptionHandler 来处理 SQLDataIntegrityExceptions(就像您是“代码”插入示例一样)。

      使用涵盖(id、名称、代码、状态)的聚合主键,这样您就可以拥有一个同名的组织,但一个是“CURRENT”,一个是“REMOVED”

      【讨论】:

      • 那么,如果还有其他有效状态 SUSPENDED 等呢?关键是我不能用简单的约束来做到这一点......我是否应该总是为每个复杂的约束编写复杂的触发器,从而复制我用 Java 编写的内容?还有如何优雅地从违反这些约束中恢复过来?我在哪里可以找到遵循这些做法的良好代码示例?
      • 事务范围是一个工作单元,工作单元没有在DAO级别定义,一般来说在DAO级别有@Transactionnal注解是一种设计味道
      • @Gab 非常不同意 - 如果不接近数据库交互的位置,您认为应该在哪里定义事务信息?
      • 好吧,你可能不同意,那是你的问题 :) 我建议你在这个主题上搜索一下,Java-ee 实践在这个问题上非常明确,我们正在谈论逻辑事务,这可能意味着其他资源比数据库。否则只看stackoverflow.com/questions/1079114/…
      猜你喜欢
      • 2012-03-25
      • 1970-01-01
      • 2011-07-07
      • 1970-01-01
      • 2013-02-17
      • 2016-04-13
      • 1970-01-01
      • 2016-05-19
      • 1970-01-01
      相关资源
      最近更新 更多