Skip to content

Instantly share code, notes, and snippets.

@ufuk
Last active September 5, 2023 05:00
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ufuk/7896273e373a18a0ce855b7132479c2c to your computer and use it in GitHub Desktop.
Save ufuk/7896273e373a18a0ce855b7132479c2c to your computer and use it in GitHub Desktop.
Distributed lock solution for Spring projects, using aspect and Redis
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;
}
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();
}
}
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