Java 执行次数有限的业务服务操作的高效线程安全实现
我有一个Meeting类,它在一个由RESTAPI接口的Spring Boot应用程序中使用JPA hibernate持久化到DB,我对必须是线程安全的操作有性能顾虑。这是会议课: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
@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 );
}