Aggregate and Transaction Management

990 단어·2 분·원문(.md)

Aggregate and Transaction #

When two users simultaneously modify an aggregate, consistency is broken.

A transaction is needed when two users simultaneously modify an aggregate. Although the operator thread and customer thread conceptually refer to the same aggregate, they physically use different aggregate objects. In this situation, each thread reflects its modifications to the DB when it commits its transaction. At this point, the consistency of the aggregate is broken.

To prevent consistency from being broken, one of the following two approaches must be taken:

  • Prevent the customer from modifying the aggregate while the operator is viewing the shipping information and changing its status.
  • If the customer's information changes after the operator has viewed the shipping information, ensure the operator re-fetches the aggregate before making modifications.

In addition to DBMS-supported transactions, there are additional transaction techniques for aggregates: pessimistic locking and optimistic locking.

Pessimistic Locking #

Pessimistic locking carries the risk of deadlocks.

Pessimistic locking is a method where the thread that first acquires an aggregate prevents other threads from modifying that aggregate until it finishes using it.

Thread 2 is blocked until Thread 1 releases the lock on the aggregate. Since Thread 2 acquires the aggregate after Thread 1 commits its transaction, Thread 2 will see the changes made by Thread 1. Pessimistic locking typically uses row-level locks provided by the DBMS. It provides a locking mechanism, using queries like for update, that allows only one connection to access a specific record. Spring Data JPA uses the @Lock annotation to specify the lock mode. Hibernate uses a for update query when PESSIMISTIC_WRITE is used as the lock mode.

Pessimistic Locking and Deadlocks #

Deadlocks must be carefully managed.

As the number of users increases, more threads fall into a deadlock state, rendering the system unresponsive. To prevent this, a maximum wait time should be specified when acquiring a lock. The javax.persistence.lock.timeout hint specifies the lock acquisition wait time in milliseconds, raising an exception if exceeded. Since hints may not be applied depending on the DBMS, it's necessary to check if the relevant functionality is supported. Spring Data JPA allows specifying query hints using the @QueryHints annotation.

Optimistic Locking #

Optimistic locking verifies if changes are possible using versions.

Pessimistic locking does not solve all transaction conflict issues.

This describes a situation where a customer changes the shipping address while an operator is viewing the shipping information and changing the delivery status. Instead of preventing simultaneous access, optimistic locking is needed to check if changes are possible at the point when modified data is actually reflected in the DBMS. A numeric type property to be used as the aggregate version must be added.

UPDATE aggtable SET version = version + 1, colx = ?, coly = ? WHERE aggid = ? and version = current_version

The query is executed only if the version of the aggregate to be modified matches the current aggregate's version. If the modification succeeds, the version is incremented by 1; if the version values differ, the modification fails.

JPA can implement optimistic locking using the @Version annotation. When using Spring's @Transactional annotation, an OptimisticLockingFailureException is thrown if a conflict occurs at the end of the transaction. Applying optimistic locking to the initial conflict scenario results in the following flow:

@Controller
public class OrderAdminController {
	private StartShippingService startShippingService;

	@RequestMapping(value = "/startShipping", method = RequestMethod.POST)
	public String startShipping(StartShippingRequest startReq) {
		try {
			startShippingService.startShipping(startReq);
			return "shippingStarted";
		} catch(OptimisticLockingFailureException | VersionConflicException ex) {
			// 트랜잭션 충돌
			return "startShippingTxConflict";
		}
	}
	... 

The following code handles OptimisticLockingFailureException thrown by the Spring framework and VersionConflicException thrown by the application service. VersionConflicException indicates that someone has already modified the aggregate, while OptimisticLockingFailureException indicates that someone modified it almost simultaneously.

Forced Version Increment #

When an entity other than the aggregate root is changed, JPA does not update the version value because the aggregate root's own value remains unchanged. However, from an aggregate perspective, if a component of the aggregate changes, the aggregate itself has logically changed.

To handle this issue, JPA uses LockModeType.OPTIMISTIC_FORCE_INCREMENT when executing a query to force a version increment at the end of the transaction.

Offline Pessimistic Locking #

Offline pessimistic locking prevents the modification screen itself from being executed if someone is already viewing it.

To strictly prevent data conflicts, offline pessimistic locking prevents the modification screen itself from being executed if someone is already viewing it. This cannot be implemented with pessimistic locking, which applies only within a single transaction, or with optimistic locking, which checks for version conflicts later.

In such a situation, if User A does not perform the modification request, the lock will not be released, so a lock expiration time must be set.

Once the lock's validity period expires, the lock should be released to allow other users to acquire it again.

However, if a modification request is performed shortly after the validity period has passed, it will fail.

To prevent this, a mechanism to periodically extend the validity period is required.

  • Attempt to acquire lock
  • Verify lock
  • Release lock
  • Extend lock expiration time
public interface LockManager {
  LockId tryLock(String type, String id) throws LockException;  // Attempt to acquire lock
  void checkLock(LockId lockId) throws LockException;   // Verify lock
  void releaseLock(LockId lockId) throws LockException;   // Release lock
  void extendLockExpiration(LockId lockId, long inc) throws LockException;  // Extend lock expiration time
}

Functions executed after acquiring a lock must verify its validity, considering the following situations:

  • If the lock's validity period has expired, another user might have acquired the lock.
  • If a user who has not acquired the lock attempts to execute a function, the function execution must be prevented.

Implementing LockManager using DB #

Table creation query for storing lock information

create table locks (
  `type` varchar(255),
  id varchar(255),
  lockid varchar(255),
  expiration_time datetime,
  primary key (`type`, id)
) character set utf8;

create unique index locks_idx ON locks (lockid);
Back-End/ddd/transaction.md