对于有大流量通过的接口,类似秒杀;为限制用户在一次活动中只能秒杀一件商品,在多机部署秒杀服务的情况下可以使用分布式锁来将用户id与商品id进行绑定并上锁,接下来介绍如何使用注解 + aop + Redisson实现复用性极强的分布式锁
导入必要依赖
<!-- 非必须 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 非必须 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.8</version>
</dependency>
<!-- 必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 必须 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
定义注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/** 防止redis的key发生冲突,所以会对key加上一些统一的前缀,例如:insertXxx, DeleteXxx */
String lockName() default "";
/** SPEL 表达式,需要通过 SpelUtil.java 工具类来解析 */
String key() default "";
/** 过期时间 */
int expire() default 5000;
/** 时间单位 */
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
切面处理
被 @RedisLock 所注解的方法,会被 RedisLockAspect 进行切面管理,代码如下:
import cn.hutool.core.util.StrUtil;
import com.example.redisson.annotation.RedisLock;
import com.example.redisson.util.RedissonUtil;
import com.example.redisson.util.SpelUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class RedisLockAspect {
@Autowired
private RedissonUtil redissonUtil;
/** 分布式锁前缀 */
private static final String REDISSON_LOCK_PREFIX = "redisson_lock:";
@Around(value = "@annotation(redisLock)", argNames = "joinPoint,redisLock")
public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
log.info("进入 RedisLockAspect 切面");
String lockKey = getRedisKey(joinPoint, redisLock.lockName(), redisLock.key());
log.info("解析后:{}", lockKey);
Object result = null;
try {
// 尝试获取锁,等待5秒,自己获得锁后一直不解锁则在指定时间后自动解锁
boolean lock = redissonUtil.tryLock(lockKey, redisLock.timeUnit(), 5000, redisLock.expire());
if (lock) {
log.info("线程:{},获取到了锁", Thread.currentThread().getName());
Thread.sleep(10000);
log.info("======获得锁后进行相应的操作======" + Thread.currentThread().getName());
//执行方法
result = joinPoint.proceed();
redissonUtil.unlock(lockKey); //释放锁
log.info("=========释放锁========" + Thread.currentThread().getName());
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 将 spel 表达式转换为字符串
*
* @param joinPoint 切点
* @return redisKey
*/
private String getRedisKey(ProceedingJoinPoint joinPoint, String lockName, String spel) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
Object target = joinPoint.getTarget();
Object[] arguments = joinPoint.getArgs();
return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + SpelUtil.parse(target, spel, targetMethod, arguments);
}
}
相关工具类
SpelUtil 解析表达式
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
public class SpelUtil {
/**
* 支持 #p0 参数索引的表达式解析
* @param rootObject 根对象,method 所在的对象
* @param spel 表达式
* @param method ,目标方法
* @param args 方法入参
* @return 解析后的字符串
*/
public static String parse(Object rootObject,String spel, Method method, Object[] args) {
if (StrUtil.isBlank(spel)) {
return StrUtil.EMPTY;
}
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
if (ArrayUtil.isEmpty(paraNameArr)) {
return spel;
}
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
//把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
return parser.parseExpression(spel).getValue(context, String.class);
}
}
RedissonUtil 加锁解锁
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedissonUtil {
@Autowired
private RedissonClient redissonClient; // RedissonClient已经由配置类生成,这里自动装配即可
/**
* 锁住不设置超时时间(拿不到lock就不罢休,不然线程就一直block)
* @param lockKey
* @return org.redisson.api.RLock
*/
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
/**
* leaseTime为加锁时间,单位为秒
* @param lockKey
* @param leaseTime
* @return org.redisson.api.RLock
*/
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return null;
}
/**
* timeout为加锁时间,时间单位由unit确定
* @param lockKey
* @param unit
* @param timeout
* @return org.redisson.api.RLock
*/
public RLock lock(String lockKey, TimeUnit unit, long timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
return lock;
}
/**
* 尝试获取锁
* @param lockKey
* @param unit
* @param waitTime
* @param leaseTime
* @return boolean
*/
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
/**
* 通过lockKey解锁
* @param lockKey
* @return void
*/
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
/**
* 直接通过锁解锁
* @param lock
* @return void
*/
public void unlock(RLock lock) {
lock.unlock();
}
}
配置
RedissonConfig 分布式锁配置
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Value("${redisson.address}")
private String addressUrl;
@Bean
public RedissonClient getRedisson() throws Exception{
RedissonClient redisson = null;
Config config = new Config();
config.useSingleServer()
.setAddress(addressUrl);
redisson = Redisson.create(config);
System.out.println(redisson.getConfig().toJSON().toString());
return redisson;
}
}
application.yml redis连接配置
spring:
redis:
database: 1
host: 127.0.0.1
port: 6379
redisson:
address: redis://127.0.0.1:6379
使用
@Service
public class HelloServiceImpl implements HelloService {
/** 重点:key的格式 */
@Override
@RedisLock(lockName = "insert", key = "#annoDemo.id + ':' + #annoDemo.name")
public void test(AnnoDemo annoDemo) {
System.out.println(annoDemo.toString());
}
}