Java 执行次数有限的业务服务操作的高效线程安全实现

Java 执行次数有限的业务服务操作的高效线程安全实现,java,multithreading,hibernate,jpa,Java,Multithreading,Hibernate,Jpa,我有一个Meeting类,它在一个由RESTAPI接口的Spring Boot应用程序中使用JPA hibernate持久化到DB,我对必须是线程安全的操作有性能顾虑。这是会议课: @Entity public class Meeting { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; @ManyToOne(optional = false) @JoinColum

我有一个Meeting类,它在一个由RESTAPI接口的Spring Boot应用程序中使用JPA hibernate持久化到DB,我对必须是线程安全的操作有性能顾虑。这是会议课:

@Entity
public class Meeting {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "account_id", nullable = false)
    private Account account;

    private Integer maxAttendees;
    private Integer numAttendees; // current number of attendees
    ...
}
正如你所看到的,我有一个账户实体,一个账户可以有许多相关的会议。一个会议有最多的与会者,而一个帐户有最多的计划会议,同样,一个帐户有一个maxSchedules和NumsSchedules变量

基本工作流程是:创建会议,然后安排会议,然后分别注册与会者

注意:这里的主要目标是避免超过允许的最大操作数(计划或寄存器)

最初,我更关注业务逻辑而不是性能,最初,我用于安排和注册与会者的业务服务如下所示:

@Service
public class MeetingService {
    ...

    @Transactional
    public synchronized void scheduleMeeting(Long meetingId, Date meetingDate) {
        Meeting meeting = repository.findById(meetingId);
        Account account = meeting.getAccount();
        if(account.getNumSchedules() + 1 <= account.getMaxSchedules() 
            && meeting.getStatus() != SCHEDULED) {
            meeting.setDate(meetingDate);
            account.setNumSchedules(account.getNumSchedules()+1);
            // save meeting and account here
        }

        else { throw new MaxSchedulesReachedException(); }
    } 

    @Transactional
    public synchronized void registerAttendee(Long meetingId, String name) {
        Meeting meeting = repository.findById(meetingId);
        if(meeting.getNumAttendees() + 1 <= meeting.getMaxAttendees() 
            && meeting.getStatus() == SCHEDULED) {
            meeting.setDate(meetingDate);
            meeting.setNumAttendees(account.getNumAttendees()+1);
            repository.save(meeting);
        }

        else { throw new NoMoreAttendeesException(); }
    } 
    ...
}
@服务
公务舱会议服务{
...
@交易的
公共同步作废日程会议(长会议ID、日期会议日期){
会议=repository.findById(meetingId);
Account=meeting.getAccount();

如果(account.getNumSchedules()+1可能这部分是域逻辑问题

考虑到如果没有对
账户的实际引用,就无法创建
会议
,我建议您在这里对业务逻辑稍作修改:

@Transactional
public void scheduleMeeting(MeetingDTO meetingDto) {
  // load the account
  // force increment the version at commit time.
  // this is useful because its our sync object but we don't intend to modify it.
  final Account account = accountRepository.findOne( 
      meetingDto.getAccountId(),
      LockMode.OPTIMISTIC_FORCE_INCREMENT
  );

  if ( account.getMeetingCount() > account.getMaxMeetings() ) {
    // throw exception here
  }

  // saves the meeting, associated to the referenced account.
  final Meeting meeting = MeetingBuilder.from( meetingDto );
  meeting.setAccount( account );      
  meetingRepository.save( meeting );
}
那么这段代码到底是做什么的呢

  • 使用
    OPTIMISTIC\u FORCE\u INCREMENT
    获取

    这基本上告诉JPA提供者,在交易结束时,提供者应该为该
    帐户
    发出一条更新语句,将
    @Version
    字段增加一个

    这就迫使提交其事务的第一个线程获胜,而所有其他线程都将被认为对参与方来说是迟到的,因此,由于抛出了
    OptimisticLockException
    ,除了回滚之外别无选择

  • 我们验证是否未满足最大会议大小。如果满足,则抛出异常

    我在这里举例说明,也许
    #getMeetingCount()
    可以使用
    @公式
    来计算与
    帐户
    关联的会议次数,而不是依赖于获取集合。这证明了这样一个事实,即我们不需要实际修改
    帐户
    ,任何一个都可以工作

  • 我们保存与
    帐户关联的新
    会议

    我假设这里的关系是单向的,因此在保存之前,我将
    帐户与会议关联起来

  • 那么为什么我们不需要任何同步语义呢

    这一切又回到了(1)。基本上,无论先提交哪个事务都会成功,其他事务最终会抛出
    OptimisticLockException
    ,但前提是多个线程尝试同时安排与同一帐户关联的会议

    如果对
    #scheduleMeeting
    的两个调用连续进入到它们的事务不重叠的位置,则不会出现问题


    由于我们避免使用任何形式的数据库或内核模式锁,此解决方案的性能将超过任何其他解决方案。

    回到起点,为什么
    服务
    需要是一个单例?它是一个SpringBean,因此默认情况下它是一个单例,这使得框架更加高效,特别是因为这些服务都与re连接在一起st控制器也是单例的,这样我们可以在运行大量并发服务时重用实例。但除此之外,即使服务不是单例并且每次都有一个新实例,我也不认为这能解决问题。你所有的同步都有缺陷。你不需要同步这些方法你的服务,因为它可能发生(这是非常可取的)不同的会议可以安排在同一时间。注册ATENDEE也一样:您希望让不同的ATENDEE同时注册到不同的会议。而您的同步方法恰恰避免了这一点。删除所有这些内容,让数据库来决定,最好使用乐观锁定机制(即Hibernate默认实现一个连接)。您应该这样做,因为如果不这样做,JPA提供程序将始终使用左外部连接而不是内部连接。换句话说,如果不指定optional=false,则会影响查询性能:)在控制器中捕获
    OptimisticLockException
    并让它重新向您的服务发送请求并不难。将其放入一个循环中,在中断用户失败消息之前尝试X次。我不会在服务中定义它,因为可能存在您更愿意调用scheduleMeeting的情况没有任何重试就失败了。看起来不错,但我仍然不清楚它如何能够仅对特定的帐户或会议进行选择,您在获取时强制增加版本,但是在注释字段w@version时,它是否会进行任何更新并增加版本?我们有两个以上的操作,我只显示schedule并注册同步的字段,但如果另一个操作正在更新同一帐户中的其他字段,我们不希望schedule操作失败,因为前者不会影响当前的计划数。然后在此处应用数据规范化。创建一个
    AccountMeetingDetails
    ,即
    @OneToOne
    帐户
    ,并将
    @版本
    和与该帐户相关的会议内容添加到该帐户。这样,您可以在不影响其他业务领域内容的情况下增加该版本。
    @Component
    public class LockProvider {
        private final ConcurrentMap<String, Object> lockMap = new ConcurrentHashMap();
    
        private Object addAccountLock(Long accountId) {
            String key = makeAccountKey(accountId);
            Object candidate = new Object();
            Object existing = lockMap.putIfAbsent(key, candidate);
            return (existing != null ? existing : candidate);
        }
    
        private Object addMeetingLock(Long accountId, Long meetingId) {
            String key = makeMeetingKey(accountId, meetingId);
            Object candidate = new Object();
            Object existing = lockMap.putIfAbsent(key, candidate);
            return (existing != null ? existing : candidate);
        }
    
        private String makeAccountKey(Long accountId) {
            return "acc"+accountId.toString();
        }
    
        private String makeMeetingKey(Long accountId, Long meetingId) {
            return "meet"+accountId.toString()+meetingId.toString();
        }
    
        public Object getAccountLock(Long accountId) {
            return addAccountLock(accountId);
        }
    
        public Object getMeetingLock(Long accountId, Long meetingId) {
            return addMeetingLock(accountId, meetingId);
        }
    }
    
    @Transactional
    public void scheduleMeeting(MeetingDTO meetingDto) {
      // load the account
      // force increment the version at commit time.
      // this is useful because its our sync object but we don't intend to modify it.
      final Account account = accountRepository.findOne( 
          meetingDto.getAccountId(),
          LockMode.OPTIMISTIC_FORCE_INCREMENT
      );
    
      if ( account.getMeetingCount() > account.getMaxMeetings() ) {
        // throw exception here
      }
    
      // saves the meeting, associated to the referenced account.
      final Meeting meeting = MeetingBuilder.from( meetingDto );
      meeting.setAccount( account );      
      meetingRepository.save( meeting );
    }