2017-03-08 55 views
1

我有一個使用JPA休眠在由REST API接口的春天引導應用程序保存到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和numSchedules變量類似。

的基本工作流程是:A會議創建的,那麼計劃,然後參加者單獨註冊。

注意:這裏的主要目標是避免超過允許的操作(調度或寄存器)的最大數量。

最初我更注重的是性能的業務邏輯,最初調度和登記參加我的商業服務是這樣的:

@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

回到起點,爲什麼'service'需要是單身人士? –

+0

這是一個Spring bean,默認情況下它是一個單例,這使得框架更加高效,尤其是因爲這些服務與單身的其餘控制器連接在一起,這樣我們在運行大量併發服務時重用實例。但除此之外,即使服務不是單身,我們每次都有新的實例,但我不明白這是如何解決問題的。 – raspacorp

+0

所有你的同步的東西是有缺陷的。您不需要同步服務的方法,因爲它可能發生(並且非常希望)可以同時安排不同的會議。與註冊參加者相同:您想讓不同的參與者同時註冊不同的會議。而你的同步方法恰恰避免了這一點。刪除所有這些東西並讓數據庫決定,理想情況下使用樂觀鎖定機制(即Hibernate默認實現一個)。 –

回答

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); 
} 

那麼是什麼代碼做什麼呢?

  1. 使用OPTIMISTIC_FORCE_INCREMENT取回。

    這基本上告訴JPA提供者,在交易結束時,提供者應該爲該Account字段遞增一個更新語句。

    這就是強制第一個線程提交其事務贏得所有其他人將被認爲晚到晚上,因此將無法選擇,但由於OptimisticLockException被拋出。

  2. 我們驗證未達到最大會議尺寸。如果有,我們會拋出一個異常。

    我在這裏說明,也許#getMeetingCount()可能會使用@Formula來計算與Account相關聯的會議數量,而不是依靠提取集合。這證明我們並不需要實際上修改Account任何這個工作。

  3. 我們保存與Account關聯的新Meeting

    我假設這裏的關係是單向的,因此我在將它保存之前將Account與會議聯繫起來。

那麼爲什麼我們在這裏不需要任何同步語義呢?

這一切都回到(1)。基本上無論哪個交易首先提交都會成功,其他人最終將投擲OptimisticLockException,但只有如果多線程嘗試同時安排與同一帳戶關聯的會議。

如果兩個調用#scheduleMeeting連續進入其交易不重疊的地方,則不會有問題。

此解決方案將執行任何其他解決方案,因爲我們避免使用任何形式的數據庫或內核模式鎖。

+0

看起來不錯,但它仍然不清楚它是如何僅對特定帳戶或會議有選擇性的,你在提取時迫使版本增量,但是當註釋字段w @ Version時它是不對的會採取任何更新並增加版本?我們不止這兩個操作,我只顯示日程並註冊同步的操作,但如果另一個操作正在更新同一個帳戶中的其他字段,我們不希望日程操作失敗,因爲前者不影響目前的時間表數量。 – raspacorp

+1

然後在這裏應用數據規範化。創建一個'AccountToMeetingDetails',它是一個'@ OneToOne'來'Account'並添加'@ Version'並且滿足與賬戶關聯的東西。通過這種方式,您可以在不影響其他業務領域的情況下增加該版本。 – Naros