SpringブートアプリケーションでJPA hibernateを使用してDBに永続化されるMeetingクラスがREST APIによってインターフェイスされています。スレッドセーフです。これは会議のクラスです:制限付き実行でのビジネスサービス操作の効率的なスレッドセーフ実装
@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)の場合、サービスはシングルトンインスタンスなので、複数のスレッドが2つの同期化された操作のいずれかを実行しようとしているときには、ロックが解放されるのを待つ必要があります。登録したいスレッドがブロックしてはならないことを意味し、私は問題をブロック間の操作を解いこれにより
...
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);
}
}
しかし、このアプローチは、維持することに余分な作業の多くが含まれますアカウント、ミーティングが削除されたときに使用されなくなったロックを破棄したり、それ以上の同期操作を実行できない状態になったりすることを確実にします。
それを実装する価値があるのか、それを行うより効率的な方法があるのかという疑問があります。
「サービス」はなぜシングルトンである必要がありますか? –
これはSpring Beanなので、デフォルトではシングルトンなので、フレームワークをより効率的にすることができます。特に、並行サービスの実行数が多い場合に、インスタンスを再利用するシングルトンでも同様のサービスが接続されているためです。しかしそれ以外にも、たとえサービスがシングルトンでなくても毎回新しいインスタンスがあったとしても、その問題をどのように解決できるかはわかりません。 – raspacorp
あなたの同期のすべてに欠陥があります。サービスのメソッドを同期する必要はありません。さまざまな会議を同時にスケジュールすることができます(また、非常に望ましい)ので、サービスの方法を同期する必要はありません。 atendeesの登録と同じです。異なるアテンダントが同時に異なる会議に登録してもらいたいです。同期化されたメソッドはこれを正確に回避します。理想的には楽観的なロック機構(つまり、デフォルトではHibernateが実装しています)を使用して、これらのものをすべて削除し、データベースを代わりに決定させます。 –