Nous allons voir une aide pour une implémentation de la Gestion d un lock avec PostgreSQL, avec les éléments clés pour le fonctionnement.
Entity
Première étape, créer notre objet BDD pour gérer le lock.
Cet objet contient un id, par exemple un nom de type string name
.
À des fins d’audit, on ajoute une date de création et une date de modification. La date de création sera au moment du premier insert. Et la date de modification sera la dernière prise de lock.
@Entity
@Table(name = "lock")
@Data
@NoArgsConstructor
public class LockEntity {
@Id
private String name;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
public LockEntity(String lockName) {
name = lockName;
createdTime = LocalDateTime.now();
updatedTime = createdTime;
}
}
Service
Avec notre entité, on peut réaliser le service qui va acquérir le lock et le créer au besoin :
@Service
@Log4j2
public class LockService {
private final LockRepository lockRepository;
public LockService(LockRepository lockRepository) {
this.lockRepository = lockRepository;
}
private void createLock(String lockId) {
try {
LockEntity lockEntity = new LockEntity(lockId);
// force new pour ne pas faire de select
lockEntity.setNew(true);
lockRepository.create(lockEntity);
} catch (DataIntegrityViolationException e) {
log.info("skip insertion : duplicate key on lock {}", lockId);
}
}
@Transactional(propagation = Propagation.MANDATORY)
public LockEntity acquireLock(String lockId) {
return lockRepository.acquireLock(lockId)
.map(modifyUpdatedTime())
.or(() -> {
createLock(lockId);
return lockRepository.acquireLock(lockId);
})
.orElseThrow();
}
private static Function<LockEntity, LockEntity> modifyUpdatedTime() {
return lockEntity -> {
log.info("Updating lock {}", lockEntity);
lockEntity.setUpdatedTime(LocalDateTime.now());
return lockEntity;
};
}
}
L’annotation @Transactional(propagation = Propagation.MANDATORY)
est une sécurité pour garantir que l’acquisition du lock est présente dans la transaction métier.
Nous indiquons à Spring par le mécanisme de propagation de transaction que nous souhaitons avoir une transaction ouverte, sinon notre lock n’a pas d’intérêt.
Une subtilité est présente dans la méthode de création du lock, l’appel à une méthode de l’entité LockEntity::setNew
.
Cette méthode est appelée par Spring JPA pour savoir s’il doit faire un insert
en BDD,
ou un merge de l’objet Java et l’objet persisté puis un update
.
Pour que JPA puisse savoir quelle opération il réalise, il appelle la méthode isNew()
de Persistable
.
Cet appel provoque un select
en base que nous ne souhaitons pas.
Pour limiter les updates et forcer un insert
en base de données, puisque dans notre cas nous savons que l’objet n’existe pas,
nous allons forcer l’entité à indiquer qu’elle est nouvelle.
Plusieurs solutions sont possibles, présentées sur Baeldung.
Pour réaliser cette modification, nous allons modifier notre entité LockEntity
en implémentant Persistable
comme dans l’exemple.
public class LockEntity implements Persistable<String> {
// [...] précédente implémentation
// Transient parce que ce n'est pas une colonne de notre BDD
@Transient
private boolean isNew = false;
@Override
public String getId() {
return name;
}
@Override
public boolean isNew() {
return isNew;
}
}
Repository
Le service appelle le repository pour réaliser les requêtes.
Avec JPA, il suffit d’une interface qui étend JpaRepository<TypeEntity, TypeId>
.
@Repository
public interface LockRepository extends JpaRepository<LockEntity, String> {
@Transactional(propagation = Propagation.REQUIRES_NEW)
default void create(LockEntity lockEntity) {
save(lockEntity);
}
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select lock from LockEntity lock where lock.name = :name")
Optional<LockEntity> acquireLock(String name);
}
Beaucoup de logiques sont présentes dans le repository.
Premier élément, le @Transactional(propagation = Propagation.REQUIRES_NEW)
permet de créer une nouvelle transaction pour créer le lock.
On sépare la création du lock de la pause, parce que la création n’est pas bloquante pour les autres threads.
Pour en savoir plus sur la propagation des transactions, suivre cet article de Baeldung.
Pour réaliser un select
bloquant, on utilise le mécanisme de PostgreSQL de SELECT FOR UPDATE
.
Pour indiquer à JPA qu’on veut un lock pessimiste en lecture et écriture, on ajoute l’annotation @Lock(LockModeType.PESSIMISTIC_WRITE)
.
Notre implémentation s’inspire du @NamedQuery
proposé sur Baeldung.
Appel depuis le traitement
@Service
@Log4j2
public class TraitementMessageService {
// temps de simulation du traitement
public static final int TIME_TRAITEMENT = 2000;
private final LockService lockService;
public TraitementMessageService(LockService lockService) {
this.lockService = lockService;
}
@Transactional
public void traitementMessage(String messageId, String lockName) {
log.info("start traitementMessage {}", messageId);
StopWatch timerLock = new StopWatch();
timerLock.start();
LockEntity lockEntity = lockService.acquireLock(lockName);
timerLock.stop();
log.info("{} : acquired lock id {} in {}", messageId, lockEntity, Duration.ofMillis(timerLock.getTotalTimeMillis()));
try {
Thread.sleep(TIME_TRAITEMENT);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end traitementMessage {}", messageId);
}
}
Cette implémentation permet de sécuriser l’accès concurrent à une ressource, grâce à la gestion des locks offerte par PostgreSQL et Spring JPA.