我有一个Meeting类,它在使用REST API接口的Spring Boot Application中使用JPA hibernate持久化到DB,我对必须是线程安全的操作有性能问题。这是Meeting类:
@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和numSchedules变量。
基本工作流程是:创建会议,然后安排会议,然后单独注册与会者。
注意:这里的主要目标是避免超过允许的最大操作数(计划或寄存器)。
我最初更专注于业务逻辑而非性能,最初我的业务服务用于安排和注册与会者看起来像这样:
@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(); }
}
...
}
这种方法的问题是同步方法的锁是对象(this),服务是单例实例,所以当多个线程试图执行两个同步操作中的任何一个时,它们需要等待锁被释放。
我采用第二种方法,使用分离的锁进行调度和注册:
...
private final Object scheduleLock = new Object();
private final Object registerLock = new Object();
...
@Transactional
public void scheduleMeeting(Long meetingId, Date meetingDate) {
synchronized (scheduleLock) {
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 void registerAttendee(Long meetingId, String name) {
synchronized (registerLock) {
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(); }
}
}
...
通过这个我解决了操作间阻塞问题,这意味着想要注册的线程不应该被正在调度的线程阻塞。
现在的问题是,一个用于在一个帐户中安排会议的线程不应该被尝试从另一个帐户安排会议的线程锁定,对于将与会者注册到不同的会议也是如此。同一帐户。
为了解决这个问题,我提出了一个我尚未实现的设计,但我的想法是拥有一个锁定提供程序,例如:
@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);
}
}
然而,这种方法涉及维护地图的大量额外工作,例如,当帐户,会议被删除或者它们进入无法完成同步操作的状态时,确保丢弃不再使用的锁。
问题是,是否值得实施,或者是否有更有效的方法。
答案 0 :(得分:2)
也许这部分是域逻辑问题。
如果在没有实际引用Meeting
的情况下无法创建Account
,我建议您在此处略微更改业务逻辑:
@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_FORCE_INCREMENT
获取。
这基本上告诉JPA提供者,在事务结束时,提供者应该为Account
递增@Version
字段的更新语句。
这就是迫使提交其事务的第一个线程获胜而其他所有线程被认为迟到的因素,因此只会因为OptimisticLockException
被抛出而无法回滚。
我们确认未达到最大会议大小。如果有,我们抛出异常。
我在这里说明,#getMeetingCount()
可能会使用@Formula
来计算与Account
相关联的会议数,而不是依赖于提取集合。这证明了我们不需要实际修改 Account
来实现这一目标。
我们保存与Meeting
相关联的新Account
。
我认为这里的关系是单向的,因此我会在保存之前将Account
与会议相关联。
那么为什么我们这里不需要任何同步语义?
这一切都回到(1)。基本上,无论哪个事务首先提交都会成功,其他事务最终会抛出OptimisticLockException
,但只有如果多个线程尝试同时安排与同一帐户关联的会议。
如果对#scheduleMeeting
的两次来电串行进入其交易不重叠的地方,则不会出现问题。
此解决方案将执行任何其他解决方案,因为我们避免使用任何形式的数据库或内核模式锁。