POM依赖坐标
关键引用如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.1</version>
</dependency>
RedissionConfig 配置
主要是将RedissonClient与Redis数据库关联起来
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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;
import org.springframework.core.annotation.Order;
/**
* 初始化Redission Client
*
* @author huxiang
*/
@Configuration
@Order(value = 3)
@Slf4j
public class RedissionConfig {
@Value("${console.redis.host}")
private String host;
@Value("${console.redis.port}")
private int port;
@Value("${console.redis.database}")
private int database;
@Value("${console.redis.password}")
private String password;
@Bean
public RedissonClient initRedisson() {
Config config = new Config();
//单机模式自动装配
config.useSingleServer().setAddress("redis://" + host + ":" + port)
.setDatabase(database);
if (StringUtils.isNotEmpty(password)) {
config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(database)
.setPassword(password);
} else {
config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(database);
}
// 设置全局默认看门狗机续期时间,如果在使用时不设置,则使用全局的,如果全局不设置,则使用默认的30000,单位毫秒
// 为啥要看门狗:
// 如果业务超长,运行期间自动给锁续上新的有效时间,不用担心业务时间长,锁自动过期被删掉,其实就是可重入锁
config.setLockWatchdogTimeout(2000);
log.debug("--------------Redission Client Created------------");
return Redisson.create(config);
}
}
注解类
import javax.validation.constraints.NotNull;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁注解
*
* @author huxiang
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Dislock {
/**
* 分布式锁的key,一般可以用用户id,用户token这种作为唯一的key,达到自己的锁只能被自己解的效果,如#userId
*/
@NotNull
String localKey();
/**
* 业务分类 默认为All不分类,建议分类
*
* @return
*/
String biz() default "ALL";
/**
* 锁等待时间 默认2秒
*
* @return
*/
long waitTime() default 2 * 1000;
/**
* 锁释放时间 默认2秒
*
* @return
*/
long leaseTime() default 2 * 1000;
/**
* 时间格式 默认:毫秒
*
* @return
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
Lock Key 解析器
生成lockKey,保证锁定资源的唯一性,并且只能解锁自己加的锁
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
/**
* lock key 解析器
*
* @author huxiang
*/
public class LockKeyParser {
/**
* 解析缓存的key
*
* @param proceedingJoinPoint 切面
* @param localKey lock key
* @param biz 业务分类
* @return String
* @throws IllegalAccessException 异常
*/
public static String parse(ProceedingJoinPoint proceedingJoinPoint, String localKey,
String biz) throws IllegalAccessException {
// 解析实际参数的key
String key = localKey.replace("#", "");
StringTokenizer stringTokenizer = new StringTokenizer(key, ".");
Map<String, Object> nameAndValue = getNameAndValue(proceedingJoinPoint);
Object actualKey = null;
while (stringTokenizer.hasMoreTokens()) {
if (actualKey == null) {
actualKey = nameAndValue.get(stringTokenizer.nextToken());
} else {
actualKey = getPropValue(actualKey, stringTokenizer.nextToken());
}
}
return biz + "-" + actualKey;
}
/**
* 获取参数Map集合
*
* @param joinPoint 切面
*
* @return
*/
private static Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map<String, Object> param = new HashMap<>(paramNames.length);
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
/**
* 获取指定参数名的参数值
*
* @param obj
* @param propName
* @return
*
* @throws IllegalAccessException
*/
public static Object getPropValue(Object obj, String propName) throws IllegalAccessException {
Field[] fields = obj.getClass().getDeclaredFields();
for (Field f : fields) {
if (f.getName().equals(propName)) {
//在反射时能访问私有变量
f.setAccessible(true);
return f.get(obj);
}
}
return null;
}
}
AOP切面处理类
我们想通过注解方式无感实现分布式锁,必须要用到AOP的功能,而且是环绕通知(有do 有 return)
import com.paratera.console.biz.lock.annotation.Dislock;
import com.paratera.console.biz.lock.parse.LockKeyParser;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 分布式锁AOP切面类
*
* @author huxiang
*/
@Aspect
@Component
public class DislockAspect {
@Autowired
private RedissonClient redissonClient;
/**
* 通过环绕通知实现加锁解锁
* 切入@Dislock注解的point
* @param proceedingJoinPoint
* @return
*
* @throws Throwable
*/
@Around("@annotation(com.paratera.console.biz.lock.annotation.Dislock)")
public Object arround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object object = null;
RLock lock = null;
boolean status = false;
try {
Dislock dislock = getDislockInfo(proceedingJoinPoint);
//生成lockKey,保证锁定资源的唯一性,并且只能解锁自己加的锁
String lockKey = LockKeyParser.parse(proceedingJoinPoint, dislock.localKey(), dislock.biz());
lock = redissonClient.getLock(lockKey);
if (lock != null) {
//试图加锁
status = lock.tryLock(dislock.waitTime(), dislock.leaseTime(), dislock.timeUnit());
if (status) {
//如果加锁成功,执行切点业务
object = proceedingJoinPoint.proceed();
}
}
} finally {
//判断是否需要解锁
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return object;
}
public Dislock getDislockInfo(ProceedingJoinPoint proceedingJoinPoint) {
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
return methodSignature.getMethod().getAnnotation(Dislock.class);
}
}
测试
最后测试:
起两个或者更多的服务实例,操作同一个数据库(比如扣库存),然后通过nginx将这些服务实例进行负载均衡配置,通过jmeter或者其他压力测试工具实现并发访问,比较Service方法上加@Dislock和不加的区别