Skip to content

Instantly share code, notes, and snippets.

@chenqiyue
Created December 4, 2017 08:43
Show Gist options
  • Save chenqiyue/18f14183262b9e4b52ee0767b02b9c57 to your computer and use it in GitHub Desktop.
Save chenqiyue/18f14183262b9e4b52ee0767b02b9c57 to your computer and use it in GitHub Desktop.
Implementation for Redlock: https://redis.io/topics/distlock
package com.laobai.distributed;
import org.springframework.core.NestedRuntimeException;
import java.lang.annotation.Documented;
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;
/**
* 提供方便的 redis distributed lock
* @see RedisLockSupportAspect
*
* @author cqy
* @since 2017/11/29.
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface RedisLock {
/**
* 分布式锁的key
* 支持 SpEl: method
*/
String key();
/**
* 默认超时时间
*/
int expired() default 3000;
/**
* 超时时间的单位
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
String exceptionMessage() default "请稍后重试";
class RedisLockException extends NestedRuntimeException {
private static final long serialVersionUID = -196589592790629707L;
public RedisLockException(String msg) {
super(msg);
}
}
}
package com.laobai.distributed;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import redis.clients.jedis.Jedis;
/**
* @author cqy
* @since 2017/11/29.
*/
@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
public class RedisLockAutoConfiguration {
@Bean
// @ConditionalOnBean(JedisConnectionFactory.class)
public RedisLockSupportAspect redisLockSupportAspect(JedisConnectionFactory jcf) {
return new RedisLockSupportAspect(jcf);
}
}
package com.laobai.distributed;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import javax.annotation.Nullable;
import lombok.Setter;
import redis.clients.jedis.Jedis;
/**
* Implementation for Redlock: <a>https://redis.io/topics/distlock</a>
* @author cqy
* @since 2017/11/29.
*/
@Aspect
public class RedisLockSupportAspect {
final SpelExpressionParser parser = new SpelExpressionParser();
final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
final JedisConnectionFactory jcf;
final byte[] delScript = ("if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end").getBytes();
@Setter
int defaultRetryIntervalMilli = 500;
public RedisLockSupportAspect(JedisConnectionFactory jcf) {
this.jcf = jcf;
}
@Around("@annotation(lock)")
public Object invoke(ProceedingJoinPoint pjp, RedisLock lock) throws Throwable {
String lockKey = getLockId(pjp, lock);
String lockValue = null;
long end = System.currentTimeMillis() + lock.timeUnit().toMillis(lock.expired());
RedisConnection conn = null;
boolean first = true;
try {
while (lockValue == null) {
if (end < System.currentTimeMillis()) {
throw new RedisLock.RedisLockException(lock.exceptionMessage());
}else if(!first){
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(defaultRetryIntervalMilli));
}else{
first = false;
}
conn = conn == null ? conn = RedisConnectionUtils.getConnection(jcf) : conn;
lockValue = tryLock(conn, lock, lockKey);
}
return pjp.proceed();
} finally {
releaseLock(conn, lockKey, lockValue);
RedisConnectionUtils.releaseConnection(conn, jcf);
}
}
/**
* SET resource_name my_random_value NX PX 30000
* @return lockValue or null
*/
private String tryLock(RedisConnection conn, RedisLock lock,
String lockKey) {
String uuid = UUID.randomUUID().toString();
//坑爹居然没有返回值
// conn.set(lockKey.getBytes(), uuid.getBytes(),
// Expiration.milliseconds(lock.timeUnit().toMillis(lock.expired())),
// RedisStringCommands.SetOption.SET_IF_ABSENT);
Jedis jedis = (Jedis) conn.getNativeConnection();
String r = jedis.set(lockKey, uuid, "NX", "PX", lock.timeUnit().toMillis(lock.expired()));
// null or 'OK'
return r == null ? null : uuid;
}
private void releaseLock(RedisConnection conn, String lockKey,
@Nullable String lockValue) {
if (lockValue != null) {
Object o = conn.eval(delScript, ReturnType.INTEGER, 1, keysAndArgs(lockKey, lockValue));
}
}
private byte[][] keysAndArgs(String lockKey, String lockValue) {
byte[][] keysAndArgs = new byte[2][];
keysAndArgs[0] = lockKey.getBytes();
keysAndArgs[1] = lockValue.getBytes();
return keysAndArgs;
}
String getLockId(ProceedingJoinPoint pjp, RedisLock lock) {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(pjp.getTarget(), method, pjp.getArgs(),
parameterNameDiscoverer);
return parser.parseExpression(lock.key()).getValue(context, String.class);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment