Last active
September 5, 2023 05:00
-
-
Save ufuk/7896273e373a18a0ce855b7132479c2c to your computer and use it in GitHub Desktop.
Distributed lock solution for Spring projects, using aspect and Redis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
import java.util.concurrent.TimeUnit; | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target(ElementType.METHOD) | |
public @interface DistributedLock { | |
/** | |
* Spring Expression Language (SpEL) expression for computing the cache key for lock dynamically. | |
* #args[i] for arguments. | |
*/ | |
String key(); | |
/** | |
* Timeout for lock. Default is zero and zero means never expire. | |
*/ | |
long timeout() default 0; | |
/** | |
* Time unit for timeout. Default is minutes. | |
*/ | |
TimeUnit timeUnit() default TimeUnit.MINUTES; | |
/** | |
* Message for when already locked. | |
*/ | |
String message() default "error.lock.process_is_in_progress"; | |
/** | |
* If release immediately is false, lock won't be released. | |
*/ | |
boolean releaseImmediately() default true; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import lombok.extern.slf4j.Slf4j; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.Around; | |
import org.springframework.expression.spel.standard.SpelExpressionParser; | |
import org.springframework.expression.spel.support.StandardEvaluationContext; | |
@Slf4j | |
public abstract class DistributedLockAspect { | |
protected abstract void acquireLock(DistributedLock distributedLock, String cacheKey); | |
protected abstract void releaseLock(DistributedLock distributedLock, String cacheKey); | |
@Around(value = "@annotation(distributedLock)", argNames = "proceedingJoinPoint,distributedLock") | |
public Object doUnderLock(ProceedingJoinPoint proceedingJoinPoint, DistributedLock distributedLock) throws Throwable { | |
String cacheKey = evalCacheKey(proceedingJoinPoint, distributedLock); | |
acquireLock(distributedLock, cacheKey); | |
try { | |
return proceedingJoinPoint.proceed(); | |
} catch (Throwable throwable) { | |
log.debug("Proceeding failed: ", throwable); | |
throw throwable; | |
} finally { | |
if (distributedLock.releaseImmediately()) { | |
releaseLock(distributedLock, cacheKey); | |
} | |
} | |
} | |
private String evalCacheKey(ProceedingJoinPoint proceedingJoinPoint, DistributedLock distributedLock) { | |
SpelExpressionParser parser = new SpelExpressionParser(); | |
StandardEvaluationContext context = new StandardEvaluationContext(); | |
context.setVariable("args", proceedingJoinPoint.getArgs()); | |
return parser.parseExpression(distributedLock.key()).getValue(context).toString(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import lombok.extern.slf4j.Slf4j; | |
import org.aspectj.lang.annotation.Aspect; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.data.redis.core.RedisTemplate; | |
import org.springframework.stereotype.Service; | |
@Aspect | |
@Service | |
@Slf4j | |
public class RedisDistributedLockAspect extends DistributedLockAspect { | |
private static final long LOCK_VALUE = 1L; | |
@Autowired | |
private RedisTemplate<String, Object> redisTemplate; | |
@Override | |
protected void acquireLock(DistributedLock distributedLock, String cacheKey) { | |
log.debug("Acquiring lock for key: " + cacheKey); | |
Long currentLockValue = redisTemplate.boundValueOps(cacheKey).increment(LOCK_VALUE); | |
log.debug("Current value for lock '" + cacheKey + "': " + currentLockValue); | |
if (currentLockValue > LOCK_VALUE) { | |
throw new RuntimeException(distributedLock.message()); | |
} | |
if (distributedLock.timeout() > 0) { | |
redisTemplate.expire(cacheKey, distributedLock.timeout(), distributedLock.timeUnit()); | |
} | |
} | |
@Override | |
protected void releaseLock(DistributedLock distributedLock, String cacheKey) { | |
try { | |
log.debug("Releasing lock for key: " + cacheKey); | |
redisTemplate.delete(cacheKey); | |
log.debug("Lock is released for key: " + cacheKey); | |
} catch (Exception ignored) { | |
log.error("Releasing lock for key is failed: " + cacheKey, ignored); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment